<feed xmlns='http://www.w3.org/2005/Atom'>
<title>colitur.git, branch v1.2.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=v1.2.0</id>
<link rel='self' href='https://git.labunix.xyz/colitur.git/atom?h=v1.2.0'/>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/'/>
<updated>2026-08-28T08:03:37Z</updated>
<entry>
<title>Merge branch 'cli-pretty'</title>
<updated>2026-08-28T08:03:37Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-28T08:03:37Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=d0807440309e2357d51cd56c72a443072b0ba57f'/>
<id>urn:sha1:d0807440309e2357d51cd56c72a443072b0ba57f</id>
<content type='text'>
--pretty, and the --month/--date/--today flags that make it usable: a box
spans seven lines, so the flag that improved reading had removed grepping.
</content>
</entry>
<entry>
<title>chore: 1.2.0</title>
<updated>2026-08-28T08:03:32Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-28T08:03:32Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=8ba98d24499bedbc2491bd56268ea4f69aaddb2f'/>
<id>urn:sha1:8ba98d24499bedbc2491bd56268ea4f69aaddb2f</id>
<content type='text'>
Two presentation flags and no change to what is computed. Nothing was removed
and no default output moved, so a minor bump.

The CHANGELOG entry for 1.1.0 is backfilled here: that release was tagged
without one, which would have left this file jumping 1.0.0 -&gt; 1.2.0. Its
content is taken from the v1.1.0 tag message rather than reconstructed.

README's status section claimed the OF module lived on an unreleased branch
and counted 887 tests, and its build block advertised the fast suite at ~8s
and the exhaustive sweep at ~80s. Every one of those was true when written and
none is now: 909 fast (~40s), 916 under the sweep (~8min). Stale suite timings
are a defect this project has corrected once before, in 0.10.0.
</content>
</entry>
<entry>
<title>feat(cli): --month, --date and --today narrow a report</title>
<updated>2026-08-28T08:03:27Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-28T08:03:27Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=b39c8187ef1b87eea084e3c3d2226be5cf320dca'/>
<id>urn:sha1:b39c8187ef1b87eea084e3c3d2226be5cf320dca</id>
<content type='text'>
--pretty made a report readable and, in the same stroke, ungreppable: a box
spans seven lines, so `grep 2026-03-` returns fragments of March rather than
March. The nearest working equivalent is a paragraph-mode awk incantation that
depends on the boxes happening to be blank-line separated -- knowledge no
reader should need to select a month.

The three flags are ALTERNATIVES, not a stack. Naming two exits 2 rather than
letting one quietly win, the same discipline the positional year and --year
already follow, and the same reason this program refuses a flag it cannot
honour instead of accepting and ignoring it.

--date and --today CARRY a year, so the year becomes optional on those two and
required only after the window has had its say: `colitur day --today` is a
complete command while `colitur day` still is not. A year named alongside them
must agree. The disagreement message names the flag rather than printing two
bare numbers, because with --today the second year appears nowhere on the
command line for the reader to trace.

temporal refuses --year yet accepts --date/--today including as its year
source. That is not a back door to the refused spelling: --year is a second
spelling of the positional year, which temporal deliberately does not offer,
whereas --date selects a DAY and merely happens to determine which year
contains it.

The window is a predicate over an ISO date STRING rather than over a Date.t,
because the two report shapes reach it differently -- resolved days carry a
Date.t, temporal_report carries a flat Record whose date is already text --
and one shared test is one implementation rather than two that can drift.

Accepted by day, readings, rubrics and temporal; refused by easter, emit,
table, render, publish, check, convert, lang, config and new-overlay.

--pretty had shipped undocumented in both --help and colitur(1). Both now
carry it, alongside the new narrowing section; the man page's awk example is
shown precisely so the flag that replaces it is justified.

Default output is byte-identical to 1.1.0 across seven years, five commands
and both rites, and across all five emit formats and the shipped templates.
</content>
</entry>
<entry>
<title>feat(cli): --pretty draws each day as an ASCII box</title>
<updated>2026-08-27T13:45:49Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukasz@arcofasiagroup.com</email>
</author>
<published>2026-08-27T13:45:49Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=f1d562a3a4d53a707385334ab553b8a6cad36b1b'/>
<id>urn:sha1:f1d562a3a4d53a707385334ab553b8a6cad36b1b</id>
<content type='text'>
Aligned columns made the fields legible but the days ran together -- with 365
of them the eye had nothing to catch on. Each day now gets its own box: a
heading carrying the date and the liturgical colour, then the celebration, its
rank and season, and any commemorations, each on its own line.

The box art is pure ASCII, only + - and |, never Unicode box-drawing. That is
the point rather than a limitation: this format exists to be pasted or piped
into a document, a mail or a plain-text ordo, and U+2500 and its relatives
survive that only when every stage agrees about encoding and font. +---+ has
never failed to render anywhere.

readings gets a labelled block, so a citation says what it is instead of being
the second of three bar-separated fields; the OF second reading simply omits
its row on the days without one. rubrics becomes a label/value list. temporal
is the day box minus the sanctoral it does not have.

A blank line separates consecutive boxes -- without it the bottom rule of one
day and the top rule of the next sit adjacent and read as a single doubled
line, which is the same "not distinct enough" this change set out to fix.

Alignment counts UTF-8 code points, not bytes, so "Pen~afort" and "Fremiot"
still line the right edge up at 76 columns; a byte-counting pad shears the box
by one per multi-byte character. Over-long values are truncated with a ~
rather than allowed to overflow, since a box whose right edge does not line up
is worse than a clipped name that the default output still carries in full.

Verified: no ANSI escape reaches a pipe on any of the four commands, and the
default output is byte-identical to installed 1.1.0 across all five.
</content>
</entry>
<entry>
<title>feat(cli): --pretty, for reading in a terminal</title>
<updated>2026-08-27T12:12:43Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukasz@arcofasiagroup.com</email>
</author>
<published>2026-08-27T12:12:43Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=34a36fcb0956db7f06c4b8860414283c03996293'/>
<id>urn:sha1:34a36fcb0956db7f06c4b8860414283c03996293</id>
<content type='text'>
The row commands print for awk: single-space fields, slugs, a variable tail.
That is right for the default and wrong for a person, who mostly wants to know
what today is.

--pretty gives the same four commands -- day, readings, rubrics, temporal --
aligned columns, the day's liturgical colour as a swatch, and commemorations
on their own indented line rather than lengthening the row. The colour was
already computed and simply thrown away on a terminal.

Colour is written only when stdout is a terminal, so piping or redirecting
yields plain aligned text: the alignment survives, the escapes do not, and the
swatch degrades to the colour's initial so the information is not lost with
them. NO_COLOR is honoured on PRESENCE whatever its value, which is the
convention's own rule -- treating it as a boolean is the usual way to get it
wrong.

Every other command refuses the flag rather than accepting it and doing
nothing: emit, table, render and publish already choose their shape through
--format and --template, and easter prints six key/value lines, not a grid.
An intermediate version accepted it everywhere and silently ignored it on
five commands, which is the failure mode this program refuses everywhere else.

Two things the layout had to learn. Column widths are a minimum, not a
maximum: the Latin season names run past them ("Tempus per annum ante
Septuagesimam" is 35 against 34), so pad always leaves a separator or the next
field fuses onto it -- that is how "Septuagesimam 1S. Hilarii" happened. And
the commemoration indent is measured from the row actually printed rather than
computed from the column constants, or it sits under the wrong column on
exactly the rows that have something to indent.

Presentation only: bin/pretty.ml decides nothing about what a day is, and
nothing reads it. Default output is byte-identical to v1.1.0 -- verified
across day, readings, rubrics, temporal and easter for 2026, 1583 and 9999.
</content>
</entry>
<entry>
<title>chore: 1.1.0</title>
<updated>2026-08-27T11:05:58Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukasz@arcofasiagroup.com</email>
</author>
<published>2026-08-27T11:05:58Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=4b3c18d61a243f1ec9c39e6279f7c687402b8dda'/>
<id>urn:sha1:4b3c18d61a243f1ec9c39e6279f7c687402b8dda</id>
<content type='text'>
A second rite is new functionality that breaks nothing: EF output is
byte-identical to v1.0.0, no flag was removed, and emit --format csv's
seventeenth column appears only under --rite of. Minor bump, not major.
</content>
</entry>
<entry>
<title>fix(cli): make the flag surface systematic</title>
<updated>2026-08-27T10:33:43Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukasz@arcofasiagroup.com</email>
</author>
<published>2026-08-27T10:33:43Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=32e792a0741f62fc2c0c7dcf9255408b3256fad5'/>
<id>urn:sha1:32e792a0741f62fc2c0c7dcf9255408b3256fad5</id>
<content type='text'>
An audit probed every flag against every command rather than reading --help,
and found three inconsistencies.

--rite reached only `day` and `readings`. main.ml's own comment gave the
reason -- "has not been widened to a second rite in this task" -- a scope note
that had outlived its task and hardened into apparent design. View.of_days was
already polymorphic over the rite's type parameters, so widening was plumbing,
not library work: `temporal`, `rubrics`, `emit`, `table`, `render` and
`publish` now all take it.

`temporal` was the sharpest case. Its refusal said "--rite has no effect on
`temporal`", which was false: the EF has Septuagesima and Passiontide, the OF
neither, and every EF slug is ef-prefixed, so the flag would change nearly
every line. A message claiming no effect where the effect is total is exactly
what the audit set out to find.

Three ways of naming a year (positional, --year, --from/--to) now cross-accept
additively; naming two that disagree is a usage error rather than one silently
winning.

emit --format csv gained a rite-dependent header: EF's 16 columns are
unchanged, OF gets a 17th "second" between "first" and "gospel". An earlier
task had recorded RFC 4180 as permanently blocking this; the rule constrains
one file, not a family of them.

`rubrics` keeps refusing --lang/--raw, now with its reason. An intermediate
version accepted them by adding a name column, which changed the default from
six tab-separated fields to seven and broke both existing consumers and the
byte-identical-EF rule. The asymmetry is real but principled: the row is a
date, a slug, a source keyword, two booleans and a preface key, so there is
nothing to translate and nothing to strip. `easter` refuses --rite for the
same kind of reason -- its six anchors sit at identical Easter offsets in both
rites.

EF output verified byte-identical to 8590338 across day, readings, rubrics,
temporal and easter for 2026, 1583 and 9999, and across all five emit formats.
</content>
</entry>
<entry>
<title>docs: describe the OF rite module and what its layers cannot see</title>
<updated>2026-08-26T22:56:15Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-26T22:56:08Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=85903385f8a32be3de3964f43b3d7831474d0a72'/>
<id>urn:sha1:85903385f8a32be3de3964f43b3d7831474d0a72</id>
<content type='text'>
CLAUDE.md's own Current state still read 'Plans 1-3 + the EF lectionary DONE'
and listed the OF module as future work, so the orientation doc a fresh
session reads first did not know the rite exists.

Records what the OF's three validation layers cannot see -- no published-ordo
witness at all where EF has three, identity comparison reaching ~12% of rows,
Validate.run being blind to value-level misplacement, and an allow-list entry
having twice hidden a real defect -- and scopes the six-layer Validation
section to EF, which is the only rite it describes.
</content>
</entry>
<entry>
<title>fix(of): correct the Annunciation's transfer_target slug, dead Rule 1</title>
<updated>2026-08-26T22:38:40Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-26T22:38:40Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=e26d7da141f7985fadc53d8399a69dbaa1153c94'/>
<id>urn:sha1:e26d7da141f7985fadc53d8399a69dbaa1153c94</id>
<content type='text'>
precedence_of.ml's transfer_target Rule 1 (Normae n. 60's fixed Easter+8
destination for the Annunciation falling in Holy Week) keyed off
"of-annunciation", a placeholder that never matched the real shipped
entry's own slug ("annunciation-of-the-lord",
data/of/calendar-2002.sexp). Rule 1 was therefore dead code: it could
only ever fire against test_precedence_of.ml's own synthetic candidate,
which happened to carry the identical placeholder.

Output was nonetheless correct on shipped data (latent, not live): Rule
3's general forward search structurally converges on the same Easter+8
target, because the Easter Octave occupies exactly Easter+1..+7 at band
20 and the 3-9 April window carries only memorials on the universal
calendar. The exposure is an overlay placing a proper solemnity (band
40) or proper feast (band 80) anywhere in 3-9 April, where Rule 3 would
diverge from Rule 1's fixed destination and Rule 1 would silently fail
to override it.

Fixed the slug, and updated the existing Rule-1 tests' own synthetic
candidate to the real slug so they keep exercising Rule 1 rather than
Rule 3's coincidental agreement. Added a test that tells the two rules
apart rather than only checking the output they happen to agree on: it
rigs the occupant callback to occupy Easter+8 with a synthetic
Tabula-entry-3 candidate, confirms (via a control case using a
different solemnity) that the rig genuinely forces Rule 3's search past
that date, then shows the real shipped Annunciation still lands on
Easter+8 despite the block -- only explicable by Rule 1 firing, not by
Rule 3 happening to agree.
</content>
</entry>
<entry>
<title>fix(of): break the movable-BVM-memorial tie per a CDWDS Notification</title>
<updated>2026-08-26T22:37:45Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-26T22:37:45Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=6e62e499aed1c5a31a18bcf5e9eaa7f2b0c4ff2e'/>
<id>urn:sha1:6e62e499aed1c5a31a18bcf5e9eaa7f2b0c4ff2e</id>
<content type='text'>
Mary, Mother of the Church (Easter+50) and the Immaculate Heart of Mary
(Easter+69) are both movable obligatory memorials that can land on a
FIXED obligatory memorial's own date -- e.g. 2011-06-13 and 2015-06-13,
both against Anthony of Padua. Both candidates banded identically at
Tabula III.10 (100, both universal Memoria_obligatoria), a tie band
could not break, so Precedence.resolve's kernel-level alphabetical
Slug.compare tie-break silently picked instead -- 892 days for Mater
Ecclesiae and 1109 for the Immaculate Heart across 1583-9999, all one
shape (measured against the pre-fix alphabetical order directly, zero
unexplained).

Searched the corpus rather than inventing a rule. colitur's two
previously-checked primary sources really are silent: the 2018 decree
(AAS 110, 437-438) never addresses universal-calendar coincidence, and
Normae n. 14's third clause resolves only two AD-LIBITUM memorials
colliding. A third source resolves it: the CDWDS's own "Notification on
the Memorial of the Blessed Virgin Mary, Mother of the Church" (24 March
2018 -- same dicastery and signatories as the decree; published only on
vatican.va, confirmed absent from the AAS 2018 combined index) states
that when this memorial "coincides with another Memorial of a Saint or
Blessed... the Memorial of the Blessed Virgin Mary is to prevail," and
extends the identical reasoning to the Immaculate Heart of Mary by name.
This is a rubric, not colitur's own reasoned choice.

Implemented as a sub-rank of 95, strictly between Tabula entries 9 and
10 -- the same "fits between neighbours" technique this module already
uses elsewhere -- scoped to the two named slugs rather than
[subject = Bvm], since the Notification's own reasoning is specifically
about a memorial's link to a movable solemnity of the Lord, not a
blanket Marian precedence.

Golden pin updated: test_rite_of.ml's 2011-06-13 tie now pins Mary,
Mother of the Church as observed (was Anthony of Padua, labelled "known
arbitrary"), with the header rewritten to state the outcome now rests on
the Notification, not a coin-flip. The litcal oracle allow-list gains
L10: litcal's own grade vocabulary has no representation for a 95
sub-rank (everything Memoria_obligatoria reads "Memorial"), a
representational gap in litcal, not a computational disagreement -- the
identity axis is untouched and needed no new entry.
</content>
</entry>
</feed>
