<feed xmlns='http://www.w3.org/2005/Atom'>
<title>colitur.git/bin, branch v0.5.0</title>
<subtitle>deterministic OCaml engine to compute and validate liturgical calendars for multiple rites, template-driven output to year 9999</subtitle>
<id>https://git.labunix.xyz/colitur.git/atom?h=v0.5.0</id>
<link rel='self' href='https://git.labunix.xyz/colitur.git/atom?h=v0.5.0'/>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/'/>
<updated>2026-08-18T10:37:36Z</updated>
<entry>
<title>release: v0.5.0</title>
<updated>2026-08-18T10:37:36Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-18T10:37:36Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=d5386c2121cfd5c6b73b573aa7db05b8ef21dff0'/>
<id>urn:sha1:d5386c2121cfd5c6b73b573aa7db05b8ef21dff0</id>
<content type='text'>
</content>
</entry>
<entry>
<title>release: v0.4.0</title>
<updated>2026-08-18T09:44:57Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-18T09:44:57Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=ca4287a6e99e14846f3560106156ef2b1c4b8cf2'/>
<id>urn:sha1:ca4287a6e99e14846f3560106156ef2b1c4b8cf2</id>
<content type='text'>
</content>
</entry>
<entry>
<title>release: v0.3.1</title>
<updated>2026-08-18T07:11:53Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-18T07:11:53Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=4fc8afc45a7371fdfe32f0683f7f0e26ab766faa'/>
<id>urn:sha1:4fc8afc45a7371fdfe32f0683f7f0e26ab766faa</id>
<content type='text'>
</content>
</entry>
<entry>
<title>test(cli): the 3 January pin is a BVM Saturday, not a feria</title>
<updated>2026-08-17T20:27:11Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-17T20:27:11Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=1fa4017a20dea1843e08afd257fc7798560659c4'/>
<id>urn:sha1:1fa4017a20dea1843e08afd257fc7798560659c4</id>
<content type='text'>
3 January 2026 is an unoccupied IV-class Saturday, so its office is Our
Lady's and now its Mass is too -- the second of the Missal's five, assigned
to Christmas through the Purification. The pin held the Christmastide
feria's readings, which is what colitur said before RG 309(a) was
implemented. Caught by make check on the 0.3.0 release, which is what that
gate is for.
</content>
</entry>
<entry>
<title>test(cli): pin the version's shape, not its literal</title>
<updated>2026-08-17T19:00:17Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-17T19:00:17Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=d8ce8c3806111e1d1ba54d2ef99a8ae550c7567b'/>
<id>urn:sha1:d8ce8c3806111e1d1ba54d2ef99a8ae550c7567b</id>
<content type='text'>
The release target bumps bin/main.ml and dune-project, but cli.t pinned
the literal too, so cutting 0.2.0 failed `make check` on a cram diff that
was purely the bump working correctly. Every release would have had to
edit a test for no gain.

The shape is asserted here instead. The value is already checked where it
can actually drift: the release target requires the freshly built binary
to report the version it just wrote.
</content>
</entry>
<entry>
<title>feat(cli): --overlay FILE, so a local calendar can be supplied</title>
<updated>2026-08-17T15:46:31Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-17T15:46:31Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=0a4bb4e6c3cbcafb302962d9d1e70b5a0bb554b8'/>
<id>urn:sha1:0a4bb4e6c3cbcafb302962d9d1e70b5a0bb554b8</id>
<content type='text'>
The Overlay algebra has been built and tested since Plan 2, and
Date_spec grew movable variants last commit, but there was still no way
to hand colitur a file: bin/main.ml applied exactly one overlay, the
shipped data/ef/adjustments.sexp, with the path hardcoded. This is the
plumbing.

--overlay is repeatable and ordered, and applies ON TOP of the shipped
adjustments rather than instead of them. That ordering is the whole
point: adjustments.sexp carries RG 110's own 30 June companion, the Major
Litanies, St Barbara and Rogation Wednesday, and a user file that
replaced it would silently drop all four while appearing merely to add a
local feast. Overlay.merge is last-writer-wins, so a local calendar can
still override a universal entry deliberately, by naming its slug.

The dispatch needed real argument parsing. It matched Sys.argv as an
exact array, which does not survive a repeatable flag -- two --overlay
arguments are a different array shape from one, and each further flag
would multiply the patterns again. Flags are now stripped first and the
remaining words matched as command plus year. Hand-rolled, because the
dependency list is frozen and this is fifteen lines. One defect found by
the cram suite on the first run: the unknown-option guard rejected
--help and --version, having excluded only their short forms.

Refused, not ignored, on easter and temporal. Neither reads sanctoral
data -- temporal deliberately runs the cycle before any layer exists --
so accepting the flag there and silently doing nothing is the failure
mode this project refuses everywhere else.

A file that fails to load is fatal, exactly as the shipped overlay is; a
directive naming a slug that does not exist warns on stderr and the run
continues. The second matters more for a user file than for ours: a typo
in a diocesan calendar should say so rather than quietly do nothing.

test/fixtures/overlay-example-diocesan.sexp is a worked example and is
labelled as invented, not calendar data. It carries both shapes a real
local calendar needs: a fixed-date patron, and a dedication on "the first
Sunday of October" -- which is the case the Date_spec work existed to
unlock and which lands on 4 October in 2026, 3 October in 2027, 1 October
in 2028. The dedication is I class because a church's own dedication
anniversary is I class in that church; at III class it lost to the
II-class Sunday every year, which a first draft demonstrated correctly
and uselessly.

The man page gains an OVERLAYS section saying plainly that an overlay is
applied, NOT validated: the five test layers assert things about the
shipped calendar and none of them can vouch for a file the user supplies.
This is the first feature that lets someone change what colitur computes,
and that distinction should not have to be inferred.
</content>
</entry>
<entry>
<title>feat(cli): --version, a CHANGELOG, and a release target</title>
<updated>2026-08-17T14:19:02Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-17T14:19:02Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=cc6655134e155dd0879ecb8eb009271e17488834'/>
<id>urn:sha1:cc6655134e155dd0879ecb8eb009271e17488834</id>
<content type='text'>
Groundwork for tagging. The project had no version anywhere: not in
dune-project, not in the binary, no CHANGELOG, no tags.

The version lives in two places -- dune-project, which generates
colitur.opam, and a constant in bin/main.ml, which is what --version
prints. Two rather than one because dune's watermarking only substitutes
in a release tarball, so a binary built the ordinary way from a checkout
would report a placeholder. The release target rewrites both and then
re-checks both, and finally requires the freshly built binary to report
the version it just wrote: a release that bumped one and not the other
would ship a binary disagreeing with its own package metadata.

--version is deliberately not embedded in the help text. cli.t pins
help's first line, and a version there would mean editing a test
expectation every release for no gain.

The release target mirrors lectio's, refusals included: no release from a
dirty tree, none without a CHANGELOG entry for that version, none whose
version bump silently failed to apply, and `make check` -- the full
1583-9999 sweep, not the sampling suite -- must pass before the tag is
created.
</content>
</entry>
<entry>
<title>feat(cli): a Makefile, a man page, and --help</title>
<updated>2026-08-17T14:11:25Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-17T14:11:25Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=7055723f8b79240ab2df1353eaaf3761f65414a9'/>
<id>urn:sha1:7055723f8b79240ab2df1353eaaf3761f65414a9</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>feat(cli): install the runtime data, and resolve it in three ways</title>
<updated>2026-08-17T13:45:27Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-17T13:45:27Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=c06ebb7344b7f267c325b832632f7cd4e81939e7'/>
<id>urn:sha1:c06ebb7344b7f267c325b832632f7cd4e81939e7</id>
<content type='text'>
`dune install` produced a colitur that could not run: the binary locates
its data relative to its own path, nothing installed the four .sexp files
anywhere, and an installed colitur exited 2 unable to read sanctoral.sexp.
`dune build @install` produced an empty tree.

data/dune installs the four RUNTIME files into &lt;prefix&gt;/share/colitur/ef.
Deliberately not the two allow-lists: those describe where colitur and a
comparison oracle disagree, are read only by the differential and oracle
tests, and are meaningless to a running colitur -- installing them would
ship an assertion about lectio's and missalemeum's behaviour as though it
were calendar data. No glob, on purpose: a glob would silently start
shipping them again the moment one was renamed.

Resolution now probes rather than computing one path and hoping. An
installed prefix is tried first, the build tree second, and a candidate
counts only if sanctoral.sexp is actually readable inside it -- so a
failed or half-removed install falls through to a working tree instead
of shadowing it and failing later with a per-file error.

COLITUR_DATA_DIR overrides both and NEVER falls through. This was the one
real design question and the first version got it wrong: it treated the
override as just another candidate, and a deliberately bogus value
produced a full, plausible, entirely un-flagged year computed off the
build tree. Someone who names a directory has stated an intent, and
silently calendaring off a different one is the silent substitution this
project refuses everywhere else. It is now exit 2 naming the directory.

Verified end to end, not inferred: installed to a scratch prefix, then
ran the binary from an unrelated cwd with no build tree near it, and
confirmed 2038-03-06 still resolves Perpetua and Felicitas through the
Common route. All four paths exercised -- build tree, valid override,
bogus override, and an empty share/ falling through.

Environment reads stay confined to bin/. The kernel's contract forbids
them and nothing below the CLI learns where the data came from; the
loaders take a path.
</content>
</entry>
<entry>
<title>feat(cli): colitur readings, one line per day</title>
<updated>2026-08-17T13:24:27Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-17T13:24:27Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=cba04b3033f23b5c2515c9e648f2b83344716224'/>
<id>urn:sha1:cba04b3033f23b5c2515c9e648f2b83344716224</id>
<content type='text'>
The lectionary has been resolvable since Task 4 but invisible from the
command line: `colitur day` prints no citations, so the branch's whole
deliverable could only be seen through the test suite.

A separate command rather than extra columns on `day`, which is where the
plan pointed. Its instruction was to append the Epistle and Gospel to
each row "matching the existing column style", and that turns out not to
be possible: a citation contains spaces and commas ("Ezech 34:11-16",
"Ecclus 51:1-8, 12") while a day row is space-separated with a
variable-length +slug commemoration tail, so appending them leaves the
row unsplittable -- no awk or cut field number recovers where the Epistle
ends. That is the opposite of what the row is shaped for.

So `day` keeps its format byte-identical, asserted directly in cli.t
rather than left implicit, and the citations get their own row with
" | "-delimited fields, safe for values containing spaces. Both formats
are a stopgap and say so in the source: the design calls for one schema
rendered through a logic-less template engine, and two ad-hoc formats are
easier to retire later than one overloaded format whose parsing rules
nobody wrote down.

The year walk is now shared. day_report and readings_report differ only
in how a day is printed, and the two-liturgical-year indexing -- with its
own reasoning about civil-versus-liturgical spans -- is exactly the part
that must not be duplicated and drift.

"-" for an absent part, though no EF day can currently print one: layer 2
asserts exactly one First and one Gospel on every day of every year
1583-9999. The CLI still does not assume a guarantee the kernel makes
about data rather than about types.

cli.t gains the four chain-step cases test_golden.ml pins against the
scans, so the CLI path is checked to agree with the library path, plus
the 2 January line, which reads the Circumcision's Mass under the
Missal's own ferial rubric (scan1:6523-6526) and not RG 17(a). Verified
the cram tests actually run and have teeth: corrupting one expectation
produces a diff and exits 1.

CLAUDE.md: state, test count (369, 370 with the sweep), the readings
command and why it is separate, and What's next -- the lectionary landed
early rather than inside Plan 4, so what remains there is OF's own
lectionary, not the mechanism. Chants stay deliberately unbuilt, and
Validate now rejects any part outside First/Gospel.
</content>
</entry>
</feed>
