diff options
| -rw-r--r-- | CLAUDE.md | 32 | ||||
| -rw-r--r-- | Makefile | 55 | ||||
| -rw-r--r-- | bin/main.ml | 50 | ||||
| -rw-r--r-- | man/colitur.1 | 219 | ||||
| -rw-r--r-- | test/cli.t | 32 |
5 files changed, 382 insertions, 6 deletions
@@ -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 <year>`, `temporal <year>`, `day <year>`, -`readings <year>`. +`readings <year>`, `-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 (`<prefix>/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 +# (<prefix>/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 <year> Easter, and the movable feasts anchored to it + colitur temporal <year> the temporal cycle, one line per day + colitur day <year> the resolved day identity, one line per day + colitur readings <year> the Mass reading citations, one line per day + colitur -h, --help this help + +<year> 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 (<prefix>/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 <year> | colitur temporal <year> | colitur day <year> | colitur \ - readings <year>"; + readings <year> (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 <prefix>/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 <exedir>/../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. @@ -17,7 +17,7 @@ A year outside the supported domain is rejected (exit 2): No/garbage arguments give a usage error (exit 2): $ colitur - colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur readings <year> + colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur readings <year> (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 |
