From 7055723f8b79240ab2df1353eaaf3761f65414a9 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Mon, 17 Aug 2026 16:11:25 +0200 Subject: feat(cli): a Makefile, a man page, and --help Three things the project had no answer for: how to install it without knowing dune, where to read about it, and what it does when asked. Makefile, same shape as lectio's -- PREFIX ?= $(HOME)/.local, BINDIR, MANDIR, and the '## '-comment help target -- so the two siblings are driven the same way. Every recipe wraps dune in `opam exec --`, which is the actual point of having one here: `make build` works from a plain shell with no `eval $(opam env)` first. install goes through `dune install` rather than a hand-rolled copy, because the binary finds its calendar data relative to its own path; the man page is installed separately to share/man/man1, matching lectio. install and uninstall were both run against a scratch prefix and checked: uninstall leaves zero files behind. PREFIX defaults to ~/.local because that is where lectio installs and where it actually lives on this machine, so colitur lands on an existing PATH with no shell change. An earlier install this session went to ~/opt/colitur, which was me over-applying a rule meant for third-party tools to one of the author's own projects; it has been removed rather than left as a second, staler binary competing on PATH. man/colitur.1 documents the four commands, both output formats and why they differ, COLITUR_DATA_DIR and its refusal to fall back, the data resolution order, exit statuses, and -- deliberately -- the limitations: EF only, Epistle and Gospel only with the chants unbuilt and rejected rather than guessed, and the BVM Saturday Mass-selection gap. A man page that only lists what works is half a man page. Renders clean under `groff -ww -z`, no warnings. --help prints to stdout and exits 0; a usage error prints one line to stderr and exits 2. That is the Unix convention rather than a preference: asking for help succeeded and should be pipeable, being invoked wrongly did not and must not pollute stdout. Both directions are asserted in cli.t, along with a loop confirming every command the help text advertises is one the dispatch actually accepts -- the check that catches help drifting away from the code. --- CLAUDE.md | 32 +++++++-- Makefile | 55 +++++++++++++++ bin/main.ml | 50 +++++++++++++- man/colitur.1 | 219 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/cli.t | 32 ++++++++- 5 files changed, 382 insertions(+), 6 deletions(-) create mode 100644 Makefile create mode 100644 man/colitur.1 diff --git a/CLAUDE.md b/CLAUDE.md index f49f2d8..39ca55a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -291,7 +291,11 @@ M20 added by the `ef-major-litanies` task, M18 394 not 395 accordingly). Fixtures live in `test/fixtures/` with asserted SHA-256s. **CLI**: `colitur easter `, `temporal `, `day `, -`readings `. +`readings `, `-h`/`--help`. Man page in `man/colitur.1`. + +`--help` prints to **stdout** and exits **0**; a usage error prints one line +to **stderr** and exits **2**. The distinction is asserted in `test/cli.t`, +both directions, because it is the sort of thing that silently rots. `readings` is a **separate command, not extra columns on `day`**, for a mechanical reason worth not rediscovering: a citation contains spaces and @@ -319,11 +323,31 @@ converters instead — that is reserved for `Slug`/`Lang`, whose `private string smart constructors deriving would bypass. ### Build & test + +There is a `Makefile` now (same shape as lectio's: `PREFIX ?= $(HOME)/.local`, +`## `-comment help target). Every recipe wraps dune in `opam exec --`, so make +works from a plain shell with no `eval $(opam env)` first. + +```sh +make help # list targets +make build +make test # fast suite, ~5 s (properties sample 200 years) +make check # full gate, ~2 min: every year 1583-9999, not a sample +make install # binary + calendar data + man page into ~/.local +make uninstall +``` + +`install` goes through `dune install`, not a hand-rolled copy, because the +binary finds its data relative to its own path (`/share/colitur/ef`); +the man page is installed separately, to `$(PREFIX)/share/man/man1`, matching +lectio. The installed copy is a SNAPSHOT, not a link — `make reinstall` after +pulling. + +Raw dune still works if preferred: ```sh eval $(opam env) # activate the project-local switch (run from this dir) -dune build -dune test # fast suite, ~3 s -COLITUR_EXHAUSTIVE_SWEEP=1 dune test --force # + every year 1583-9999, ~50 s +dune build && dune test +COLITUR_EXHAUSTIVE_SWEEP=1 dune test --force dune exec colitur -- day 2026 | head ``` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..75372ac --- /dev/null +++ b/Makefile @@ -0,0 +1,55 @@ +COLITUR := colitur +PREFIX ?= $(HOME)/.local +BINDIR := $(PREFIX)/bin +MANDIR := $(PREFIX)/share/man/man1 + +# The binary locates its calendar data relative to its own path +# (/share/colitur/ef), so `dune install` -- not a hand-rolled copy -- +# is what places the four .sexp files where a running colitur will look for +# them. See bin/main.ml's own [data_dir] for the full resolution order. +# +# dune needs the project-local opam switch on PATH. Every recipe that invokes +# dune goes through this, so `make` works from a plain shell with no +# `eval $(opam env)` first -- which is the whole point of having a Makefile +# over documenting the dune commands. +DUNE := opam exec -- + +.PHONY: help build test check install uninstall reinstall clean fmt man doc +help: ## show this help + @grep -hE '^[a-z-]+:.*##' $(MAKEFILE_LIST) | sed -E 's/:.*## /\t/' | sort + +build: ## build the binary + $(DUNE) dune build + +test: ## fast suite (~5s): properties sample 200 years + $(DUNE) dune test + +check: ## full gate (~2min): every year 1583-9999, not a sample + COLITUR_EXHAUSTIVE_SWEEP=1 $(DUNE) dune test --force + +install: build ## install binary, calendar data and man page into PREFIX (default ~/.local) + $(DUNE) dune install --prefix $(PREFIX) + @mkdir -p $(MANDIR) + install -m 644 man/colitur.1 $(MANDIR)/colitur.1 + @echo "installed $(BINDIR)/$(COLITUR), data in $(PREFIX)/share/colitur/ef, man page in $(MANDIR)" + @command -v $(COLITUR) >/dev/null 2>&1 || \ + echo "note: $(BINDIR) is not on PATH -- add it, or run $(BINDIR)/$(COLITUR) directly" + +uninstall: ## remove everything install put into PREFIX + -$(DUNE) dune uninstall --prefix $(PREFIX) + rm -f $(MANDIR)/colitur.1 + @echo "removed $(COLITUR) from $(PREFIX)" + +reinstall: uninstall install ## uninstall then install (the installed copy is a snapshot, not a link) + +man: ## preview the man page + man -l man/colitur.1 + +doc: ## lint the man page (groff warnings; silence means clean) + groff -man -Tutf8 -ww -z man/colitur.1 + +fmt: ## format the OCaml sources + $(DUNE) dune build @fmt --auto-promote + +clean: ## remove build artifacts + $(DUNE) dune clean diff --git a/bin/main.ml b/bin/main.ml index ca89a12..573f190 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -330,10 +330,57 @@ let resolved_year_report ~line y = let day_report y = resolved_year_report ~line:day_line y let readings_report y = resolved_year_report ~line:readings_line y +(* Help and usage are deliberately DIFFERENT things, and the difference is the + Unix convention rather than a preference: asking for help is a request that + SUCCEEDED, so [--help] prints to stdout and exits 0 (it can be piped into a + pager or grepped); being invoked wrongly is an error, so [usage] prints a + one-liner to stderr and exits 2, keeping stdout clean for whatever the + caller was really trying to capture. *) +let help_text = + {|colitur -- deterministic liturgical calendar engine (Roman rite, 1962) + +usage: + colitur easter Easter, and the movable feasts anchored to it + colitur temporal 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 -h, --help this help + + is a civil year, 1583..9999 inclusive. Each report covers 1 January to +31 December of that year, not a liturgical year. + +output formats: + day date weekday season week slug rank colour [+commemoration ...] + 2026-04-05 sunday paschaltide 1 ef-easter-sunday class-1 white + readings date slug | Epistle | Gospel + 2026-12-25 ef-nativity | Heb 1:1-12 | John 1:1-14 + + A citation contains spaces, so readings uses " | " between its fields while + day stays space-separated; that is why they are separate commands rather + than extra columns. + +environment: + COLITUR_DATA_DIR + Read the calendar data from this directory instead of the + installed (/share/colitur/ef) or build-tree location. + If it is set and holds no sanctoral.sexp, colitur exits 2 rather + than silently falling back to a different copy of the data. + +exit status: + 0 success + 2 bad usage, year out of range, or the calendar data could not be read + +Reading references only (e.g. "Jn 3:16"); never scripture text. +See colitur(1) for the full description and the sources it computes against.|} + +let print_help () = + print_endline help_text; + exit 0 + let usage () = prerr_endline "colitur: usage: colitur easter | colitur temporal | colitur day | colitur \ - readings "; + readings (try: colitur --help)"; exit 2 let with_year ys f = @@ -346,6 +393,7 @@ let with_year ys f = let () = match Sys.argv with + | [| _; ("-h" | "--help" | "help") |] -> print_help () | [| _; "easter"; ys |] -> with_year ys easter_report | [| _; "temporal"; ys |] -> with_year ys temporal_report | [| _; "day"; ys |] -> with_year ys day_report diff --git a/man/colitur.1 b/man/colitur.1 new file mode 100644 index 0000000..c65cc58 --- /dev/null +++ b/man/colitur.1 @@ -0,0 +1,219 @@ +.TH COLITUR 1 "2026" "colitur" "User Commands" +.SH NAME +colitur \- deterministic liturgical calendar and lectionary engine (Roman rite, 1962) +.SH SYNOPSIS +.B colitur +.BR easter | temporal | day | readings +.I YEAR +.br +.B colitur +.BR \-h | \-\-help +.SH DESCRIPTION +.B colitur +computes the liturgical calendar of the 1962 Roman Missal \(em the +Extraordinary Form \(em and the Mass reading citations for every day, entirely +offline and without a network. Its name is +.I computus liturgicus +crossed with the Latin +.IR colitur , +"He is worshipped". +.PP +For each day it resolves the season, the week, the observed celebration with +its rank and colour, any commemorations, any transfers, and the day's Epistle +and Gospel. It emits reading +.I references +only \(em +.RB \(lq "Jn 3:16" \(rq +\(em and never scripture text. +.PP +The engine is total and deterministic over the whole domain +.BR "1583..9999" . +It reads no clock, draws no randomness, and given the same data produces the +same answer for any year in range. Years outside the domain are refused at the +boundary rather than approximated. +.PP +Each report covers 1 January to 31 December of the civil +.IR YEAR . +That is deliberately not a liturgical year, which is Advent\-anchored and +straddles two civil years; +.B colitur +resolves both liturgical years that touch the requested civil one and prints +the civil slice. +.SH COMMANDS +.TP +.BI easter " YEAR" +Easter and the movable feasts anchored to it \(em Ash Wednesday, Palm Sunday, +Ascension, Pentecost, Corpus Christi \(em one per line, as +.RI \(lq name " " date \(rq. +.TP +.BI temporal " YEAR" +The temporal cycle alone, one line per day, before the sanctoral calendar is +resolved against it. Chiefly useful for inspecting season and week boundaries +in isolation. +.TP +.BI day " YEAR" +The resolved day identity, one line per day: the temporal cycle and the +sanctoral calendar reconciled by the rite's own rules of precedence, +occurrence, commemoration and transfer. +.TP +.BI readings " YEAR" +The Mass reading citations, one line per day. +.TP +.BR \-h ", " \-\-help +Print a usage summary to standard output and exit 0. +.SH OUTPUT FORMAT +.SS day +.RS +.nf +date weekday season week slug rank colour [+commemoration ...] +.fi +.RE +.PP +Space\-separated, with one +.BI + slug +suffix per admitted commemoration. A +.B \- +in the week column means the day carries no week number. +.RS +.nf + +2026\-04\-05 sunday paschaltide 1 ef\-easter\-sunday class\-1 white +2026\-11\-02 monday time\-after\-pentecost 23 commemoration\-of\-all\-souls class\-1 black +2057\-03\-26 monday lent 3 annunciation\-of\-the\-blessed\-virgin\-mary class\-1 white +ef\-lent\-3\-monday +.fi +.RE +.SS readings +.RS +.nf +date slug | Epistle | Gospel +.fi +.RE +.PP +A reading citation contains spaces and commas, so this report separates its +fields with +.RB \(lq " | " \(rq +where +.B day +stays space\-separated. That is the reason the citations are a separate command +rather than extra columns on +.BR day : +appended there, no field number could recover where the Epistle ended. A +.B \- +in either citation field means none was resolved. +.RS +.nf + +2026\-12\-25 ef\-nativity | Heb 1:1\-12 | John 1:1\-14 +2038\-03\-06 sts\-felicitas\-perpetua | Ecclus 51:1\-8, 12 | Matt 13:44\-52 +.fi +.RE +.PP +Both reports are one line per day and ordered by date, so they compose with +.BR grep (1), +.BR awk (1) +and +.BR join (1) +in the ordinary way. +.SH ENVIRONMENT +.TP +.B COLITUR_DATA_DIR +Read the calendar data from this directory instead of the installed or +build\-tree location. If it is set and contains no +.IR sanctoral.sexp , +.B colitur +exits 2 naming the directory; it does +.I not +fall back to another copy. Naming a directory states an intent, and quietly +computing a calendar from different data than the one requested is a failure +mode this program refuses. +.SH FILES +.TP +.I /share/colitur/ef/ +The installed calendar data: the sanctoral calendar, its one hand\-authored +overlay, the temporal lectionary and the Commons. Four S\-expression files. +.TP +.I /../data/ef/ +The build\-tree location, used when running from a source checkout. +.PP +Resolution order is +.B COLITUR_DATA_DIR +first, then the installed directory, then the build tree. A directory counts +only if it actually contains +.IR sanctoral.sexp , +so a failed or half\-removed installation falls through to a working checkout +instead of shadowing it. +.SH EXIT STATUS +.TP +.B 0 +Success. +.TP +.B 2 +Bad usage, a year outside 1583..9999, or the calendar data could not be read. +.SH EXAMPLES +Easter and its dependent feasts: +.RS +.nf + +.B colitur easter 2026 +.fi +.RE +.PP +One date: +.RS +.nf + +.B colitur day 2026 | grep '^2026\-12\-25' +.fi +.RE +.PP +Every first\-class day of a year: +.RS +.nf + +.B colitur day 2026 | awk '$6 == "class\-1"' +.fi +.RE +.PP +Days carrying at least one commemoration: +.RS +.nf + +.B colitur day 2027 | grep '+' +.fi +.RE +.PP +Run against a checkout's data rather than the installed copy: +.RS +.nf + +.B COLITUR_DATA_DIR=~/git/projects/colitur/data/ef colitur day 2026 +.fi +.RE +.SH SOURCES +The calendar is computed against the 1962 +.I Missale Romanum +and its +.IR "Rubricae Generales" , +which are the sole authority for what +.B colitur +emits. Published calendars \(em missalemeum, Divinum Officium, gcatholic \(em +are used as comparison oracles in the test suite only: a divergence from one +is flagged loudly and adjudicated against the Missal, never silently adopted. +Several such divergences have been resolved in this engine's favour. +.SH LIMITATIONS +Only the Extraordinary Form (1962) is implemented; the Ordinary Form is a +planned peer module, not an overlay of this one. +.PP +Only the Epistle and the Gospel are emitted. The chants \(em Psalm, Gradual, +Tract, Alleluia, Sequence \(em are deliberately not computed: they have no +source in this engine's data and no oracle to validate them against, and the +engine rejects any citation part outside those two rather than emit one it +cannot stand behind. +.PP +The votive Office of the Blessed Virgin Mary on Saturday is kept, but the +seasonal selection among its five Masses is not yet implemented, so its +reading citations fall back to the day's ordinary ones. +.SH SEE ALSO +.BR lectio (1) +.SH LICENSE +AGPL\-3.0\-or\-later. diff --git a/test/cli.t b/test/cli.t index 288356c..d7e86f7 100644 --- a/test/cli.t +++ b/test/cli.t @@ -17,7 +17,7 @@ A year outside the supported domain is rejected (exit 2): No/garbage arguments give a usage error (exit 2): $ colitur - colitur: usage: colitur easter | colitur temporal | colitur day | colitur readings + colitur: usage: colitur easter | colitur temporal | colitur day | colitur readings (try: colitur --help) [2] The EF temporal cycle for a year, one line per day: @@ -211,3 +211,33 @@ with a confusing per-file error. (Exercised directly in the task, by creating _build/default/share/colitur/ef and confirming the year still resolves; not reproduced here because the cram sandbox's own exe path makes the layout awkward to stage without asserting on dune internals.) + +Help and usage are different things, and the difference is the Unix +convention rather than a preference. Asking for help is a request that +SUCCEEDED: it goes to standard output and exits 0, so it can be piped into a +pager or grepped. Being invoked wrongly is an error: a one-liner to standard +error, exit 2, leaving stdout clean for whatever the caller was really trying +to capture. + + $ colitur --help | head -1 + colitur -- deterministic liturgical calendar engine (Roman rite, 1962) + + $ colitur -h | head -1 + colitur -- deterministic liturgical calendar engine (Roman rite, 1962) + + $ colitur --help > /dev/null + $ colitur -h > /dev/null + +Nothing on stderr, and exit 0 (an exit other than 0 would print a [N] line): + + $ colitur --help 2>&1 >/dev/null + +The error path is the mirror image -- nothing on stdout, exit 2: + + $ colitur bogus 2>/dev/null + [2] + +Every command the help lists is a command the binary actually accepts. This +is the check that catches help text drifting away from the dispatch: + + $ for c in easter temporal day readings; do colitur $c 2026 > /dev/null || echo "$c FAILED"; done -- cgit v1.3