<feed xmlns='http://www.w3.org/2005/Atom'>
<title>colitur.git/lib/kernel, branch v0.8.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.8.0</id>
<link rel='self' href='https://git.labunix.xyz/colitur.git/atom?h=v0.8.0'/>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/'/>
<updated>2026-08-19T11:34:53Z</updated>
<entry>
<title>feat(naming): the language table</title>
<updated>2026-08-19T11:34:53Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-19T11:34:53Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=45bcbde502e07ad73b96039fcbb62471a3c94781'/>
<id>urn:sha1:45bcbde502e07ad73b96039fcbb62471a3c94781</id>
<content type='text'>
Maps strings to strings and nothing else -- no calendars, no dates, no
filesystem. That is what lets every command use it without the kernel
learning about presentation.

Every lookup is total, and a miss returns THE KEY rather than the empty
string. A partial translation is therefore usable from its first line,
and the fully-degraded case is exactly today's output (bare slugs) rather
than a blank page.

--raw is a real identity table, not a special case threaded through every
call site: one value the whole program passes around.

Reuses Overlay_ini's INI reader rather than growing a second one that
would drift in its comment, quoting and trimming rules; parse_sections is
exposed in the .mli for that, with no behaviour change.

Fixes one defect found while running the brief's own tests rather than
transcribing them blind: weekday's internal lookup key is an English
day-name word (month's is already the numeral string), so on a miss it
echoed that word instead of the documented numeral, breaking both the
0=Sunday convention and Lang.raw's own identity contract for weekday.
weekday/month now fall back to string_of_int n directly on a miss
instead of through get's generic echo-the-search-key path; month is
byte-identical since its key already equals string_of_int n.
</content>
</entry>
<entry>
<title>feat(overlay): a flat INI front end, which verifies its own output</title>
<updated>2026-08-18T11:55:18Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-18T11:55:18Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=36daf47dde9b0c16dacef31163ea74effdd5e7f3'/>
<id>urn:sha1:36daf47dde9b0c16dacef31163ea74effdd5e7f3</id>
<content type='text'>
A convenience format for calendars that add a few local feasts and drop one
or two universal entries. Section names are slugs, a [overlay] section carries
the id, and status/subject/layer default so the common case -- an ordinary
local saint's feast -- says only what distinguishes it.

It is a FRONT DOOR, not a second data model. It parses to exactly the
Overlay.t the S-expression form parses to, and everything downstream is the
same code on the same values; a test asserts an INI overlay and its
hand-written sexp equivalent produce identical Overlay.t values. It is also
deliberately less expressive -- Add, Suppress and single-field Edit only --
and refuses Replace, multi-field edits and citation edits BY NAME rather than
dropping them silently. Anything it cannot say is a reason to write sexp.

Little of this is new machinery: tools/bootstrap_sanctoral.ml has parsed INI
and mapped it to celebrations since the sanctoral was bootstrapped from
lectio. The dates needed extending, since that mapping handled only MM-DD;
the flat forms are easter+N/easter-N and mon/day/nth, with nth negative to
count from the end.

`colitur convert` is a separate step rather than --overlay sniffing the
extension, so the author can read what their INI became. When a date form was
mistyped, "what did the engine actually get" is the question, and an invisible
transpile cannot answer it.

The conversion verifies its own output: the emitted text is parsed back with
the same function that loads an overlay and must equal what the INI denoted,
or nothing is written. That is the point of the module. A transpiler emitting
valid-but-wrong sexp is the failure a convenience format invites, and `colitur
check` could never catch it -- the output would parse cleanly and mean
something else.

That check was WRONG on the first attempt, in exactly the way it exists to
prevent. It re-serialised the parsed value instead of parsing the text being
returned, so it verified t -&gt; sexp -&gt; t, which is true by construction and
proves nothing. Found by mutation: corrupting the renderer to emit a different
overlay id sailed through and exited 0. It now parses the returned text, the
mutation is caught with exit 2, and two tests fail under it where none did
before.
</content>
</entry>
<entry>
<title>feat(overlay): default citations and layer, humanise parse errors</title>
<updated>2026-08-18T11:13:40Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-18T11:13:40Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=8d0ab547895460956e36b66b47a299b02a3474f0'/>
<id>urn:sha1:8d0ab547895460956e36b66b47a299b02a3474f0</id>
<content type='text'>
A user-supplied overlay is the only sexp this engine reads that a human writes
by hand, and two of Celebration.t's eight fields carry nothing such an author
can meaningfully supply: citations is always empty for a local feast, since
citations come from the rite's lectionary and never from calendar data, and
layer merely repeats the overlay file's own id.

Requiring both made the commonest first mistake -- omitting them -- fail with
'lib/kernel/celebration.ml.t_of_sexp: the following record elements were
undefined: citations layer', which names a source file the author will never
open and does not say what to write instead. A minimal local feast needed 12
lines, two of them noise.

Overlay.load now fills each field only where ABSENT, so an explicitly stated
value always wins: an overlay may legitimately name a layer different from its
own id, and defaulting must not silently overwrite that. A test pins both
directions.

Deliberately scoped to overlays. Layer.load, which reads the shipped
sanctoral, is untouched and stays strict -- that data is the project's own,
every field of it is asserted by tests, and a missing one there is a defect
rather than a convenience.

Parse failures also stop naming kernel source paths: the five prefixes that
actually reach a user are rewritten into the vocabulary of the file being
edited, and anything unrecognised passes through verbatim rather than being
reworded into something possibly wrong.
</content>
</entry>
<entry>
<title>feat(kernel): movable Date_spec variants, and Rogation Wednesday</title>
<updated>2026-08-17T15:22:43Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-17T15:22:43Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=2b9f8133a22ce6dc309644a98bbe61507f574b38'/>
<id>urn:sha1:2b9f8133a22ce6dc309644a98bbe61507f574b38</id>
<content type='text'>
Date_spec carried one variant, Fixed(month, day), and its own header said
Sunday- and Easter-relative forms would arrive with the OF sanctoral. They
arrive early because two things needed them at once: a user-supplied
overlay carrying a local movable feast had no way to express "the first
Sunday of October", and Rogation Wednesday's commemoration has been
recorded as architecturally blocked since 2026-08-13.

That blockage turns out to have been a premise, not a fact. The register
concluded the Rogation "is not Commemoration_only sanctoral data by
nature... it needs a genuinely THIRD kind of thing this architecture has
no name for" -- but the reasoning rested on there being "no civil (month,
day) pair to anchor a Fixed entry to". That is a statement about
Date_spec's expressiveness. With Easter_offset the entity is ordinary
sanctoral data, the identical shape Add major-litanies already ships,
differing only in how the date is written. No third channel was needed.
The register is corrected rather than left contradicting the code.

Two variants: Easter_offset of int, and Nth_weekday of {month; nth;
weekday} with negative nth counting from the end. Fixed's sexp
representation is untouched, so all 327 sanctoral entries and the whole
overlay parse unchanged. Both are re-validated in t_of_sexp through their
smart constructors, extending the idiom Fixed already used -- the failure
that guards against is invisible, a spec deserialising into something
that silently never resolves and a celebration vanishing with no
diagnostic.

Easter is supplied by the rite, not computed in the kernel. Computus
ships Gregorian and Julian both, and picking one here would hard-code a
Roman assumption into rite-agnostic code and be silently wrong for a
Julian-reckoning rite. Rite.t gains an easter field; Rite_ef supplies the
Gregorian one.

Layer keeps a split index. Fixed entries stay in the year-independent
(month, day) table, which preserves the fast path and the "30 November
counted twice in a 371-day span" behaviour validate.mli documents for St
Andrew. Movable entries have no year-independent key by construction --
the same spec lands on a different (month, day) each year -- so they
resolve per civil year into a rata-die table. index_by_date is removed
rather than kept as an alias: two ways to index a layer, one of which
silently ignores movable entries, is the trap this change exists to
avoid.

Two domain-edge defects, both found by the suite rather than reasoning. A
liturgical year is Advent-anchored, so resolving civil year y names y +/-
1, and at the edges those are 1582 and 10000, which Computus correctly
refuses by raising. The ceiling surfaced through the domain-ceiling test,
the floor through `colitur day 1583` failing outright. Fixed once, in
Layer.index, the single point that calls the rite's easter -- a clamp
repeated per caller would have been two places to get wrong a third time.

Rogation Wednesday: RG 87 assigns the Litaniae minores to the Monday,
Tuesday AND Wednesday before Ascension (scan1:691); colitur built the
first two from the start and never the third. RG 88 keeps it out of the
Office, so the Ascension Vigil remains observed. RG 89 routes it through
the Major Litanies' rules, but RG 109's closed list names only maiores,
so by RG 107 it is an ORDINARY commemoration -- confirmed in the output,
not assumed. Name from the Missal's own "feria IV" (scan1:20495-20497),
violet to match the Monday and Tuesday offices.

Blast radius, full 1583-9999 sweep against the pre-change binary: 2257
days, every one the same shape, zero unclassified, line counts equal both
sides. The complement is the informative half -- in the other 6160 years
the Rogation does not appear at all, because RG 111 admits one
commemoration on a II-class day and an impeded feast takes it under RG
113, a Commemoration_only candidate having no row in RG 91's table. Both
shapes are pinned.

Allow-lists unmoved: layer 3 compares no commemorations and is blind by
construction, and layer 4's 2026-2027 window has a competing feast on
both Wednesdays, so the Rogation is capped out in exactly the years the
oracle can see. C8 is unchanged, asserted by its own count pin.

Not built: Sunday-relative specs, which nothing needs yet, and the
--overlay plumbing that would let a user actually supply a local movable
feast. Nth_weekday therefore ships with synthetic and property coverage
only; inventing a shipped entry to exercise it would be fabricating
calendar data.

Register: section 6.10.
</content>
</entry>
<entry>
<title>test: lectionary properties, golden pins, and the blast radius</title>
<updated>2026-08-17T12:56:46Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-17T12:56:46Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=947cadfab527829f8e42a896339a0fa6c2e81ad9'/>
<id>urn:sha1:947cadfab527829f8e42a896339a0fa6c2e81ad9</id>
<content type='text'>
Layers 2 and 5 for the reading citations, which until now had coverage
from layers 3 and 4 only -- both oracle-bounded, at 2050 and 2027.
test_validate.ml said so in its own words ("No fixture here exercises
citations") and test_golden.ml's describe pinned twelve fields, none of
them a citation. 358 tests -&gt; 369, and 370 under the sweep.

Layer 2 as two Validate checks rather than standalone QCheck properties,
so they ride the existing prop_invariants and the exhaustive sweep
instead of duplicating that machinery. "citations" asserts a day's parts,
sorted, are exactly [First; Gospel] -- one condition carrying three
invariants: zero or two but never one, nothing outside First and Gospel
since the chants are deliberately unbuilt, and no part twice, which a
bare cardinality test would miss. "citations-unresolved" is the separate
coverage half: the chain fell through every step. Both are gated on the
rite producing some citation somewhere, so a rite whose lectionary is not
built is held to neither and the synthetic fixtures stay unaffected.

The result over the whole domain: every day of every liturgical year
1583-9999 resolves exactly one Epistle and one Gospel. Mutation-proved
live rather than merely silent -- treating a well-formed pair as
malformed reddens the suite with real days showing [first,gospel].
citations-unresolved has no live witness at all, which is why
test_validate.ml gains five fixtures driving both checks through a new
?readings override, including the gate itself.

Layer 5 as six pins, one per chain step, so a regression names the step
that broke. Every literal was read out of the scans before being
compared with colitur, per that file's standing rule; scan lines are in
each pin's comment. Two of the six cover step 4, and the plan's own dates
for steps 1 and 4 were wrong: 2030-01-13 is a temporal day (Holy Family
on the generic Sunday slug) and would not have exercised step 1, while
2038-03-08 is John of God, who has a full proper Mass and never routes
through a Common -- so he becomes the step-1 pin instead.

A separate formatter rather than widening describe: widening would
invalidate all thirty existing literals at once and the only practical
way back would be pasting in whatever colitur printed, which is what the
file's header forbids. Restoring them honestly means hand-verifying
thirty more Masses against the scans, a task of its own. Stated rather
than hidden, and defensible because layer 2 now checks the shape of
every day in every year, which is the breadth that widening would have
bought.

Two pins earned their keep immediately. Lent I Monday failed on "Ezek"
against colitur's "Ezech" -- notation only, and colitur matches the
Missal's own abbreviation; layer 3 normalises this away and cannot see
it. The displaced-Sunday feria pins M26 shape 2(b)'s known-wrong
walkback deliberately, labelled as such so a future fix fails loudly,
and established two things M26 had not recorded: both parts come from
the displacing feast, not only the Gospel it cites, and the reference
carries a stray trailing period inherited through the bootstrap.

Blast radius: zero. The CLI never calls Validate, no data file changed,
and 20 years spanning 1583-9998 produce 7301 byte-identical lines
between the pre-change binary and HEAD.

Step 4's blind spot is now measured rather than argued. Corrupting the
Common of Non-Virgins I's Epistle leaves layer 3 (16801 days) and layer
4 (730 days) both green, reddening only one unit test and one new pin.
Both step-4 pins matched the scans on the first attempt -- the first
independent confirmation that route has ever had. It narrows the gap
without closing it: two of five Common-routed saints are witnessed,
isidore-of-seville is not, and gregory-the-great and patrick are
unreachable in any year 2005-2050.

Register: section 6.7 addendum. validate.mli documents both new labels;
liturgical_day.mli's "always empty until Plan 4" was outlived by the
lectionary and is corrected.
</content>
</entry>
<entry>
<title>kernel+ef: resolve readings, chain steps 1 and 2</title>
<updated>2026-08-14T22:16:35Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-14T22:16:35Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=124d7e2261c721c4a49f7203efc276088c7cb217'/>
<id>urn:sha1:124d7e2261c721c4a49f7203efc276088c7cb217</id>
<content type='text'>
Liturgical_day.citations has read "always empty until Plan 4" since Plan 3;
it is now filled. Rite.t gains a readings function, rite-supplied for the
same reason transfer_target is: what a day with no proper falls back to is a
rubric, not a universal. Calendar calls it and passes its own temporal
function as the callback the rite needs to reach another date.

Steps 1 and 2 only: the observed celebration's own proper, else the day's own
temporal slug. Nothing encodes "Lent has daily propers" -- the presence of an
entry is the discriminator.
</content>
</entry>
<entry>
<title>kernel(lectionary): fix round 1 -- load never raises</title>
<updated>2026-08-14T14:52:29Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-14T14:52:29Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=2c0187f42cc45864932aba2529f876be86ddba39'/>
<id>urn:sha1:2c0187f42cc45864932aba2529f876be86ddba39</id>
<content type='text'>
Sexplib.Sexp.load_sexp raises bare Failure for several malformed inputs
(unterminated list/string, empty file, more than one sexp) rather than
Sexplib.Sexp.Parse_error, so those cases escaped Lectionary.load as an
uncaught exception -- breaking the .mli's own promise and the kernel's
never-raises-on-fallible-construction constraint. Mirrors the catch-all
already present in Layer.load and Overlay.load, plus a second catch-all
on the t_of_sexp branch for defence in depth.

Adds test_load_never_raises, covering all of the above plus a missing
file, using Filename.temp_file rather than a hardcoded path. Verified the
new test fails against the pre-fix load (uncaught Failure) and passes
against the fix.
</content>
</entry>
<entry>
<title>kernel(lectionary): slug-keyed reading citations</title>
<updated>2026-08-14T14:43:00Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-14T14:43:00Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=bca1a206589bc41bbcb367a7ae9b217c02f0e892'/>
<id>urn:sha1:bca1a206589bc41bbcb367a7ae9b217c02f0e892</id>
<content type='text'>
Data only, the same shape and discipline as Layer: slug-canonical,
duplicates rejected at construction naming the offending slug, sexp
round-trips. Which slug a day falls back to is a rubric and belongs to the
rite module, so nothing here knows about ferias or Sundays.
</content>
</entry>
<entry>
<title>docs: a confidence raised while its revisit trigger was deleted</title>
<updated>2026-08-13T19:18:58Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-13T19:18:58Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=72b19098905cd609d87783f6e68311de4f1b61a0'/>
<id>urn:sha1:72b19098905cd609d87783f6e68311de4f1b61a0</id>
<content type='text'>
Five follow-ups from the fix-round re-review, none touching a computed
result.

M20's note had said "moderate-high, not certain" with a specific revisit
trigger attached. The fix round raised the label to "near-certain" and
deleted the trigger in the same edit. Upgrading a confidence while
removing the condition that would lower it again is the one move this
record must not make, so the trigger is restored alongside the higher
label: no primary text anywhere names the Major Litanies in a
Mass-orations-count worked example, only the general II-class-Sunday
rule twice over. RG 434(b) closes the Office-shaped doubt and nothing
further.

"WORD-IDENTICAL" overstated the relation between RG 111(b) and n.
434(b). Only the trailing "quae tamen omittitur si commemoratio
privilegiata facienda sit" is verbatim in both; the openings differ,
n. 434(b) recasting the rule into the orations register. The claim was
self-refuting -- both texts are quoted adjacent to it -- and the
argument never depended on it. Now "identical in its operative clause".

And n. 434 is not "a different part of the same document": the running
heads put RG 111 under Rubricae generales and n. 434 under Rubricae
generales Missalis Romani, two distinct corpora bound in one volume,
which is the entire force of the corroboration. The code comment had
understated its own point.

Also corrects the register's LT line range for n. 434(b) (3574-3576, not
3564-3570; the (b) clause is not in the cited range), and records in
calendar.ml the diagnostic that channel (3) trades away: a rite whose
transfer_target names a date its own disposition omits used to raise a
loud, mislabelled Validate failure and is now silent at the origin. The
kernel cannot distinguish that from a deliberate omission without rite
knowledge it must not have, so accepting it is right -- but the signal
is gone, and that should be written down rather than discovered later.
</content>
</entry>
<entry>
<title>fix(kernel): a transferred candidate can settle by being capped out, too (fix round 1, F1)</title>
<updated>2026-08-13T18:55:28Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-13T18:55:28Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=10f0e964ceb2999b330ca4b3c6545e24eeb71f6c'/>
<id>urn:sha1:10f0e964ceb2999b330ca4b3c6545e24eeb71f6c</id>
<content type='text'>
The prior fix (settled_at) recognised two settlement channels for a
transferred candidate at its target -- winning outright (observed) or
surviving as a commemoration -- but missed a third: reaching the target
and then being CAPPED OUT there, by admit's own RG-111-style admission
count limit or by disposition's own Omit. That candidate lands in the
target's own omitted list, genuinely settled and accurately labelled, but
settled_at did not check that list, so the origin reported it as
unresolved under the same wrong, hardcoded unconverged_reason -- the exact
original bug, one level further out.

Unreachable on shipped EF data (the Major Litanies are the only privileged
Commemoration_only candidate real data carries, and no second one can ever
share Easter+2), but reachable by construction: a second privileged
Commemoration_only entry on the Litanies' own transfer target that
outranks it in admit's Class1 selection, or -- without any synthetic data
-- forcing the Litanies' own RG 109(f) privilege to Ordinary, which makes
the transferred candidate lose that same cap against its own real target.

Fixed by adding target-omitted membership as a third disjunct in
settled_at. New regression test in test_calendar.ml, built the same way:
the real EF layer plus one synthetic privileged Commemoration_only entry
on the real 2011 transfer target, sorting ahead of the Litanies so it wins
the Class1 slot. Mutation-verified to fail specifically when the third
disjunct is removed. COLITUR_EXHAUSTIVE_SWEEP=1 dune test --force stays
clean after the fix, confirming it changes no shipped day's output.
</content>
</entry>
</feed>
