<feed xmlns='http://www.w3.org/2005/Atom'>
<title>colitur.git, branch v1.1.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.1.0</id>
<link rel='self' href='https://git.labunix.xyz/colitur.git/atom?h=v1.1.0'/>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/'/>
<updated>2026-08-27T11:05:58Z</updated>
<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>
<entry>
<title>fix(render): derive citation keys from the day, not a hardcoded pair (W5)</title>
<updated>2026-08-26T22:24:03Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-26T22:24:03Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=bf42e85ed622da5227a69c26ec801d32c767bb32'/>
<id>urn:sha1:bf42e85ed622da5227a69c26ec801d32c767bb32</id>
<content type='text'>
lib/render/view.ml hardcoded exactly two template keys, "first" and
"gospel", regardless of what a day's citations actually contain. The OF
now resolves a third reading, Second, on Sundays and solemnities (OLM
1981 Praenotanda n. 69.1/n. 84(b)(c)) via the rite-supplied
Rite.t.citation_shapes, which the render layer could not express. Latent
today (bin/main.ml's reject_rite_for refuses --rite on every command that
reaches View.of_days, so only EF ever reaches this path), but the wrong
shape to carry forward.

View.citation_fields now derives one key per Citation.part ACTUALLY
PRESENT in a day's own citations, in Citation.all_parts order, using
Citation.part_to_string for the key name. An absent part contributes no
key at all, not a key holding the empty string: Template.render already
treats a missing key and a present Str "" identically in Var, Section and
Inverted position, so nothing is lost, and Emit_json.year dumps the whole
Obj verbatim, so an unconditionally-present-but-empty key for a
still-unbuilt part (Psalm/Tract/Alleluia/Sequence) would sit in every EF
day's JSON forever. Filtering to present parts alone is what makes EF's
output identical byte for byte BY CONSTRUCTION rather than by a rite
check: EF's citations are always exactly [First; Gospel], so the derived
key set is always {"first"; "gospel"}, in that order.

padding_cell keeps its own fixed "first"/"gospel" pair rather than
deriving from a (nonexistent) day: it mirrors the closed set every rite
currently reachable through this render path (EF alone) always carries,
documented as debt for whenever a rite whose citation_shapes includes
Second is admitted here -- test_padding_and_real_share_key_set would
catch the mismatch immediately.

Downstream consumers: Emit_json needed no change (already a verbatim
dump). Emit_xml and Emit_ics gained a third, order-preserving element/line
(a &lt;citation part="second"&gt; element; a "Second ..." DESCRIPTION line
spliced between Epistle and Gospel) since neither is a fixed-column
format. Emit_csv is deliberately left unwidened and commented: RFC 4180's
header-row contract makes a variable column count illegal, and OF cannot
reach `emit` today regardless, so there is nothing to widen for yet;
admitting OF here later needs a disclosed, deliberate format change, not
a silent one.

term_keys (line ~239) is left unchanged, with a comment recording the
judgment call: "epistle" and "lesson" are indeed EF/Roman vocabulary
(Epistola vs. a non-apostolic Lectio, e.g. an Ember day's Old Testament
reading) sitting in an otherwise rite-agnostic list, but nothing forces a
fix yet -- no OF template exists, and every in-repo EF template already
reads term.epistle, so renaming it would break 11 shipped templates and
any user template for zero present benefit. "lesson" already sits unused,
already translated in both lang files, and is the natural neutral term a
future OF ordo template would reach for.

Tests: test_view.ml gains test_ef_days_never_carry_a_second_key (a whole
year, not one day) and test_three_citation_day_exposes_all_three, driving
View.of_days directly with a hand-built Liturgical_day.t since OF cannot
reach this path through the CLI yet -- the only honest way to exercise a
latent path. test_emit.ml/test_ics.ml extend coverage to Emit_xml/Emit_ics
(presence, value, and reading order) and pin Emit_csv's deliberate
16-column non-widening.

EF byte-identity: zero diff in data/ef/ or lib/rites/rite_ef/ against the
pre-change commit. `colitur day`, `readings`, `emit --format csv/xml/json/
ics`, and `table --template templates/ef/ordo.txt` for 2026, all non-empty,
all byte-identical (cmp) against a worktree build of the pre-change commit.
</content>
</entry>
<entry>
<title>fix(citation): represent chapter-crossing verse ranges (W4)</title>
<updated>2026-08-26T21:53:58Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-26T21:53:49Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=90584d87e763789808329847c46dbaa8e22dca47'/>
<id>urn:sha1:90584d87e763789808329847c46dbaa8e22dca47</id>
<content type='text'>
Parse.verse_range's [last] endpoint gains an optional chapter
(Parse.verse_end: { chapter : int option; verse : verse_num }), so a
hyphen range whose two endpoints lie in different chapters
("1 John 1:5-2:2") can be represented at all. Rejected: a bare
[int option] living alongside [last] as a second field on
verse_range -- that would let 'a chapter with no verse' exist as a
constructible value.

parse_part now splits a part's leading "chapter:" at the FIRST
colon only (not every colon), so the verses side can itself carry a
second colon from a crossing range. parse_range detects a crossing
by checking whether the range's right-hand side contains ':', and
skips the same-chapter descending-range guard for that case (a later
chapter is always "ahead", whatever its own verse numbers are).
Handles the compound shape too -- a crossing range followed by
further, same-chapter verse references in the same comma list
("Matthew 9:35-10:1,5a,6-8") -- since those trailing pieces parse as
ordinary bare verses/ranges, unaffected by the preceding crossing.

Render's one_range renders a crossing [last] through the same
chapter_verse template one_part already uses for the part's own
leading "chapter:verses", so a style that reconfigures the
chapter/verse separator renders a crossing endpoint in that same
convention rather than a hardcoded ':'.

This closes the W4 known-wrong: 41 (now 49, after an intervening
Second-reading extraction) of the OF lectionary's citations printed
unconverted, every one this exact shape. test_citation_coverage_of.ml's
pinned residual is now empty and asserted exactly, over the full
1725-field data/of/lectionary.sexp population, including the round-trip
check (parse -&gt; render -&gt; parse structural equality). test_citation.ml
gains direct parse-suite cases for the basic crossing, the compound
shape, a mid-list crossing, a crossing with a sub-verse letter, and a
malformed-crossing rejection.

data/of/lectionary.sexp is regenerated via its own generator
(tools/bootstrap_lectionary_of.ml, whose own embedded header text is
updated to match); only comment lines change, confirmed by diff --
no lectionary entry differs. test_lectionary_of.ml's whole-file
SHA-256 pin is updated to match.

EF is unaffected: data/ef/ is untouched since v1.0.0, and a direct
byte comparison of `colitur day`/`colitur readings` for 2026, 1583
and 9999 against a git-worktree build of 1c0137d is identical on all
six outputs.
</content>
</entry>
<entry>
<title>fix(of): dispatch Holy Family's Normae n.35(a) fallback (W2)</title>
<updated>2026-08-26T21:45:16Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-26T21:45:16Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=9ebb06983b7a26db5564302253f2551dfbcf834e'/>
<id>urn:sha1:9ebb06983b7a26db5564302253f2551dfbcf834e</id>
<content type='text'>
temporal_of.ml's temporal only ever reached holy_family's own correct
26-31 December / 30 December fallback logic through sunday_slug, which
returns None immediately for a non-Sunday date. In a year 25 December
is itself a Sunday (26-31 December then holding no Sunday of its own),
the feast vanished entirely and fell through to an ordinary Christmas
ferial slug -- confirmed on 2022, 2033, 2039, 2044, 2050 (and roughly
1583..9999 domain-wide, one year in seven).

temporal's dispatch now consults holy_family directly, independent of
weekday, between the sunday_slug and christmas_feria_slug branches,
carrying the identical identity the Sunday case already builds
(subject Lord, holy_family_names, rank Festum). holy_family and
anchors were already correct and needed no change.

Added a direct example-based test for the fallback across five
affected years plus a control for the unaffected Sunday case, and a
domain-wide property (Holy Family is observed in every liturgical
year 1583-9999) folded into both the 200-sample QCheck run and the
exhaustive sweep -- the property that would have caught this directly.

Un-pinned the now-fixed behaviour everywhere it was recorded as
known-wrong: test_rite_of.ml's own 1583 pin (rewritten to assert the
fix), test_validate_of.ml's own independent exhaustive-sweep counter
and landmark-year filter (found via make check, not the initial
survey), and data/of/expected-divergences-litcal.sexp's L2/L3
allow-list entries against the litcal oracle (closed and removed, with
a dedicated regression test replacing the allow-list's own silence).
</content>
</entry>
<entry>
<title>fix(of): anticipate St Joseph onto 18 March per Normae n. 56(f)</title>
<updated>2026-08-26T21:40:17Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-26T21:40:17Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=2f6d25aff88b4c76c61e36889f1c017b3cc8faee'/>
<id>urn:sha1:2f6d25aff88b4c76c61e36889f1c017b3cc8faee</id>
<content type='text'>
W1: when 19 March (St Joseph) falls on Palm Sunday, colitur sent him
forward to Easter+9 instead of anticipating the solemnity backward to
18 March, the one rule in the whole Normae transfer machinery that
names an earlier date. Live in 16 of 1583-2100 (1595, 1606, 1617,
1690, 1758, 1769, 1780, 1815, 1826, 1837, 1967, 1978, 1989, 2062,
2073, 2084).

Blocked on a kernel contract: Rite.t.transfer_target required its
result be strictly later than the impeded date, an obligation argued
purely from the EF's RG 96. Traced against Calendar's actual placement
pass rather than assumed unsafe: the round loop re-resolves the whole
year fresh every round, and its two safety nets (~start/~stop; the
round-count guard) both test the target's own value with no comparison
against origin, so a bounded backward jump converges the same way a
bounded forward one does. The one requirement that stays load-bearing
is target &lt;&gt; origin: Precedence.resolve never deduplicates by slug, so
a candidate returning its own argument unchanged would collide with
itself in the same round's contest. Relaxed the obligation accordingly
(rite.mli) and added the fourth transfer_target branch in
Precedence_of, ahead of the existing three (Precedence_of.transfer_target,
Rule 0). calendar.ml needed no logic change, only two comments that
asserted "every genuine transfer moves forward" as a blanket fact.

Unpinned the two tests that asserted the old wrong (forward) behaviour
and replaced them with assertions of the correct one, plus a new
direct unit test of the backward branch across two independent years.

EF output is untouched: data/ef/ and lib/rites/rite_ef/ carry no diff,
and colitur day/readings for 2026, 1583 and 9999 are byte-identical
against a build of the prior commit. OF's own 2005-2050 window carries
none of the 16 affected years and shows zero unexpected diffs; the
full 1583-9999 domain still reaches a fixed point under the exhaustive
sweep.
</content>
</entry>
<entry>
<title>fix(cli): render the Second reading on colitur readings</title>
<updated>2026-08-26T12:51:55Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-26T12:51:55Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=1c0137dee8ff3858707281366662891a5ff01044'/>
<id>urn:sha1:1c0137dee8ff3858707281366662891a5ff01044</id>
<content type='text'>
The OF lectionary (data/of/lectionary.sexp) carries 183 Second readings,
146 of them on Sunday keys, and the kernel resolves them correctly -- but
readings_line (EF) and readings_line_of (OF) in bin/main.ml only ever
printed Citation.First and Citation.Gospel, so `colitur readings --rite of`
silently dropped every Second reading it computed.

Both functions gain a second_suffix lookup, spliced between the existing
First and Gospel fields, present only when the day carries a Second
citation. No rite check: EF's citation_shapes is [[First; Gospel]] alone,
so EF rows are unaffected by construction, not by special-casing.

EF byte-identity verified two ways: `git diff --stat v1.0.0..HEAD --
data/ef/` is empty, and `colitur day`/`colitur readings --rite ef` for
2026, 1583 and 9999 cmp identical against a build of the prior commit.

test/cli.t: fixed the one now-stale assertion and added coverage for the
three-citation OF Sunday shape alongside the unchanged two-citation OF
weekday and EF shapes.
</content>
</entry>
</feed>
