| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Writing a local calendar had no feedback loop. An overlay is applied, not
validated -- that stays true, and the five test layers still cannot vouch for
a user's file -- but before this the only way to learn whether yours did what
you meant was to generate a year of output and grep for your own slug, and the
only way to see that a directive matched nothing was to notice a warning
scroll past among 365 lines.
`check` loads each overlay, applies it to the real shipped calendar, and
reports the directive counts, the slug each one targets, and any directive
that found no target. It exits 2 when a file fails to load or a directive
matched nothing, so it composes into a Makefile or a pre-commit hook rather
than merely being readable. It is applied to the SHIPPED calendar and not to
an empty layer on purpose: against an empty one every Suppress would fail
trivially and the check would be worthless.
It answers three narrow questions -- does the file parse, does every directive
find its target, what does the merged result contain. It does not validate a
calendar against the rubrics and cannot, and both the help text and the man
page say so rather than letting the name imply more than it does.
`new-overlay` prints a starter to stdout for redirection, rather than writing
a file where it likes. Every value in it is a placeholder that will appear in
`day` output if left unedited, so a half-finished overlay is visible rather
than silently inert, and it documents the three date shapes and the legal
values for each closed field inline.
load_ef_layer now returns its diagnostics instead of printing them: day and
readings still want them on stderr beside a year of output, while check wants
them on stdout, attributed to the overlay that produced them, and counted.
Printing at the source made the second impossible.
The cram test round-trips new-overlay through check rather than pinning the
template line by line -- editing its prose should not fail a test, but a
syntax error in it still must.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`--overlay` shipped last week with nothing to point at. The only example
was a test fixture, explicitly labelled invented and living where no user
would find it, so "colitur supports local calendars" was true but
undemonstrated.
data/ef/examples/diocesan-example.sexp is installed to
<prefix>/share/colitur/examples/ and is runnable documentation: every
celebration in it is invented, with example- slugs that cannot collide,
and its header explains the four directives, the three date shapes, and
the field vocabularies before showing any of them.
It demonstrates the range deliberately rather than the minimum: a
fixed-date local patron with its own propers; a movable dedication on the
first Sunday of October, I class because a church's own dedication
anniversary is I class in that church and at III it would lose to the
Sunday every year; an Easter-relative commemoration; an Edit raising a
universal feast for local use; and a Suppress shown commented out,
because suppressing a universal feast is a real liturgical act and an
example should not invite it casually.
Two things the header says plainly, since this is the one file that lets
someone change what colitur computes: an overlay is applied ON TOP of the
shipped calendar and never instead of it, and an overlay is applied, NOT
validated -- the five test layers assert things about the shipped data
and cannot vouch for a user's file.
A test asserts it loads against the real EF rank vocabulary, still
demonstrates at least four directives, and applies with NO diagnostics.
The last matters most: a directive naming a slug the universal calendar
lacks would warn, and an example that warns teaches the wrong lesson. It
deliberately does not pin which days it produces -- those celebrations
are invented, and pinning their dates would make an illustrative file
behave like calendar data.
|
| |
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`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 <prefix>/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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The register was transcribed faithfully but was itself wrong: RG 96's
Attamen (a), primary-source-verified from the scans and now corrected
in the register, reads 'festum Annuntiationis B. Mariae Virg., quando
est transferendum post Pascha, transfertur ... in feriam II post
dominicam in albis' -- the Monday-after-Low-Sunday seat applies ONLY
'quando est transferendum post Pascha', when the feast is to be
transferred PAST EASTER. The unconditional transcription made the
exception fire on every impeded Annunciation regardless of cause.
transfer_target now computes the general RG 96 target first, for every
candidate, and overrides to the Monday after Low Sunday only when that
general target itself falls after Easter Sunday -- testing the rubric's
own condition directly rather than re-deriving a date-proximity rule
from first principles.
Confirmed against three real years the review named: 2007, 2012 and
2057 all previously sent the Annunciation to Easter + 8 (16 April,
16 April, 30 April respectively) when the correct, now-produced target
is the next free day before Easter (26 March in each case -- Passion
Sunday in 2007/2012, Lent III Sunday in 2057). Verified with actual CLI
output for all three, before and after.
Also cites RG 96 Attamen (b), the same primary-source passage, as the
direct authority for All Souls' own move to the following Monday when
impeded by a Sunday -- previously inferred from RG 91 entry 8's
parenthetical plus the general walk, which happened to produce the
right date; now stated directly.
Rewrote the two existing Annunciation unit tests, whose synthetic
occupants no longer trigger the (now correctly conditional) exception,
and added a 2057 regression test using the real Temporal_ef.temporal as
occupant plus a real-data cram pin -- both mutation-verified against
the unconditional reading.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Assembles Rite_ef.context (lib/rites/rite_ef/rite_ef.ml[i]): temporal,
anchors and vocab from Temporal_ef; rules from Precedence_ef's band,
disposition and admit; season_runs = Vocab_ef.seasons; transfer_target
newly implemented here.
transfer_target (RG 96): the next following day that is not I or II
class, with the Annunciation's own exception (Monday after Low Sunday).
Terminates by a structural step bound on its internal search, independent
of Calendar's own round guard, which bounds rounds across a year, not one
call's walk; documented as an obligation on rite.mli's transfer_target
field, which did not previously state it.
Fixes the vigil-naming mismatch Task 7's review predicted: the sanctoral
bootstrap names its vigils with a vigil-of-X prefix (lectio's own
convention), while Precedence_ef's is_vigil only recognised the temporal
cycle's own X-vigil suffix. Both are now recognised, fixing RG 91 entries
21/26 and RG 33's vigil omission for the four affected celebrations.
Verified by unit test and by mutation-testing the fix (reverting it fails
exactly the new rows) and against real output across several years.
Suppresses data/ef/sanctoral.sexp's vigil-of-christmas via a new overlay,
data/ef/adjustments.sexp: it is the same celebration as the temporal
cycle's own ef-nativity-vigil, both dated 24 December.
colitur day <year>: one line per civil-year day, temporal and sanctoral
fully resolved through Layer, Overlay, Precedence_ef and Calendar -- the
first CLI path exercising the whole Plan 3 pipeline against real data.
Verified the All Souls transfer chain (Tasks 7-8-11) end to end against
real output for both a Sunday year (2025, lands on 3 Nov) and a
non-Sunday year (2026, observed directly on 2 Nov).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
week is "" for roughly 30 days a year (any day outside a numbered
week). Printed as-is among seven space-separated fields, that collapses
two fields into a double space, so naive field-position parsing (e.g.
awk '{print $4}') silently reads the wrong column on those days -- a
real defect for a project whose stated design is Unix-composable CLIs.
Emit "-" instead, so every line always has exactly seven
single-space-separated fields.
Record.headers/to_row exist for exactly this kind of column-safe
encoding but use a different column set and order than this CLI's
documented date/weekday/season/week/slug/rank/colour format (design
spec ยง6); adopting them here would be a larger, unrequested format
change, so this keeps the CLI's own field list and only substitutes
the empty value.
The cram test's pinned lines are re-verified against liturgical
anchors before promoting, not blind-promoted: 1 Jan 2026 (Circumcision,
class-1, white) and 2-3 Jan (ordinary class-4 Christmastide ferias,
the finding-1 slug fix already re-pinned separately) are correct;
2026-04-05 remains the sole Easter Sunday line, Paschaltide week 1,
class-1, white, matching the independently-computed Easter anchors
printed by `colitur easter 2026` immediately above in the same file.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two correctness bugs in Temporal_ef, both only visible across many
years, not a single point assertion:
1. Duplicate slugs within one liturgical year. Christmastide has no
numbered weeks, so the ferial fallback's <season>-<week>-<weekday>
scheme collapsed every feria in it to literal week "0". Since
colitur's Christmastide runs 25 Dec - 13 Jan (RG 72-73, a deliberate
divergence from lectio), the same weekday recurs across that span
and the keys collided (e.g. ef-christmas-0-saturday on three
different dates). Fixed with a dedicated christmastide_feria_slug
that splits the span into four sub-stretches, prioritising
compatibility with lectio's own keys wherever lectio has one:
- 26-28 Dec keeps lectio's existing ef-christmas-0-<weekday>.
- 2-5 Jan becomes ef-christmas-1-<weekday> -- lectio collapses
this indistinguishably into the same key as the stretch above,
so there is nothing to preserve; a colitur-only lectionary gap.
- 7-13 Jan splits at the actual first-Sunday-after-Epiphany
origin: on/after it, this is genuinely week 1 of Time after
Epiphany and takes lectio's own ef-time-after-epiphany-1-<weekday>
(which also can't collide with that season's own later week-1
ferias, since it's the same computation). Before it (0-6 days,
whenever Epiphany doesn't fall on a Saturday), a first attempt
at labelling this "week 1" too, matching a literal reading of
lectio's behaviour, was verified empirically (a throwaway sweep
of the full 1583..9998 domain) to reproduce duplicates in most
years -- so this remainder is its own ef-christmas-2-<weekday>,
a further colitur-only gap.
Verified with the same throwaway sweep: zero duplicate slugs across
1583..9998 after the fix.
2. named's week field was set by hand on some branches (Passion/Palm
Sunday, Easter, Low Sunday, Pentecost and its Vigil, Christ the
King) and left at None on others (Ascension and its Vigil, Corpus
Christi, Sacred Heart) even though all of them sit inside a
numbered season run. named no longer carries a week at all --
temporal now calls week itself for every day, named or not, so "a
named day inside a run carries that run's week" holds by
construction rather than by remembering to set it on each branch.
temporal.mli's week field doc is reworded to state the actual rule.
Covering tests: point assertions for all four Christmastide
sub-stretches; a QCheck property scanning random years for any
duplicate slug within a liturgical year (excluding the deliberate
resumed-Sunday reuse); point assertions for the four previously-None
days now carrying their week (first to fail without the fix: Ascension
Vigil, expected Some 6, got None); a QCheck property asserting
temporal's week equals week for every day of the year, replacing a
prior property whose fallback made it structurally incapable of
detecting an omitted week.
Also: cite RG 91 e18 for the September/Advent Ember days matching
lectio and flag the Lent/Whitsun Ember and Rogation slugs as
colitur-only lectionary gaps inline, matching the existing
Nativity-vigil/octave-day convention; cite RG 117/123/127/128/131 for
season colours and Gaudete/Laetare rose; drop the unreachable
Passiontide arm from the Sunday-rank match (Passiontide has only two
Sundays and both are already named above, so no Passiontide Sunday
ever reaches that fallback).
|
| |
|
|
|
|
|
| |
One line per day -- date, weekday, season, week, slug, rank, colour -- built
through Record, the canonical output view. Until Plan 3 brings the differential
and oracle layers, this dump is how a human checks EF temporal against
missalemeum by eye.
|
|
|
Argv-parsed subcommand; validates the 1583..9999 domain (exit 2 otherwise);
prints 'name YYYY-MM-DD' lines. Cram test covers the output and the exit codes.
|