aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CLAUDE.md32
-rw-r--r--Makefile55
-rw-r--r--bin/main.ml50
-rw-r--r--man/colitur.1219
-rw-r--r--test/cli.t32
5 files changed, 382 insertions, 6 deletions
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 <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.
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 <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