<feed xmlns='http://www.w3.org/2005/Atom'>
<title>colitur.git/lib/render/emit_ics.ml, 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-26T22:24:03Z</updated>
<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>feat(cli): --lang and --raw, colitur lang, colitur config</title>
<updated>2026-08-20T07:28:08Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-20T07:28:08Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=0f94c388880972f8e665ed01a6b38190bcc61499'/>
<id>urn:sha1:0f94c388880972f8e665ed01a6b38190bcc61499</id>
<content type='text'>
Names are Latin by default now; --raw restores the old byte-exact output,
verified against a pre-branch binary built from a clean HEAD checkout, not
merely asserted. day and readings gain a trailing display-name field
(day's own name suffix, readings' own " | name" suffix) rather than a
substitution in the slug's existing position: a name contains spaces and
would otherwise break the space-separated columns, the same reasoning that
already keeps readings a separate command. The field is present only when
it differs from the slug already shown, which is a direct consequence of
Lang.raw being the identity table (no lookup ever misses), not a special
case threaded through day_line/readings_line -- under --raw, or any
language with no entry for a given day, the trailing field is simply
absent, byte for byte what this project printed before naming existed.

An unknown language is an error naming what is available, never a silent
fallback to Latin: a booklet quietly printed in the wrong language is worse
than one that refuses to print. A declared fallback still chains, so a
partial translation shows its fallback language rather than slugs.
lang_dir mirrors data_dir's probe order exactly -- installed prefix, then
build tree -- because an installed binary that finds calendar data but no
language files is the failure this would otherwise ship.

Config (lang/overlay/template/format) is loaded once, unconditionally,
before dispatch: a config file that exists but fails to parse is fatal on
every command, not only the ones that consult it, because a config the
user wrote and colitur cannot honour is not something to silently carry on
past. An unknown key or unknown section is reported and ignored, worded
differently so a misspelled section reads differently from a misspelled
key inside a recognised one. overlay's config default applies only when
zero --overlay flags were given, never merged with a partial CLI list, so
precedence stays flag &gt; config &gt; default in one direction throughout.

colitur lang --list/--dump/--check and colitur config --show are what make
"anyone can write a language file" true rather than merely permitted, the
same pattern the overlay system already established with new-overlay and
check. --check reports what is missing AND rejects an unknown slug (exit
1), because a key matching nothing is silently dead and its author would
otherwise never learn why their own name does not appear. --dump emits
sections in a fixed order (Lang.keys' own global sort keeps every
section's keys contiguous), so two dumps of the same table are
byte-identical. config --show resolves through the same Config.resolve
every other command uses -- deliberately no separate provenance function,
so the two cannot disagree -- and previews exactly what flags given on the
same command line would resolve to anywhere else.

One authorized change outside this task's own file list: emit_ics.ml's
SUMMARY line was reading the unlocalised rank/colour pair ("class-1,
white") beside an already-localised name, where the JSON schema right next
to it correctly carries rank_name/colour_name ("II classis"/"albus").
Found while reviewing the published site; fixed at the source.
</content>
</entry>
<entry>
<title>feat(render): names reach the view and every emitter</title>
<updated>2026-08-19T13:29:33Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-19T13:29:33Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=6d367ab8e90f6e713d262a7f19fb908b28d4796a'/>
<id>urn:sha1:6d367ab8e90f6e713d262a7f19fb908b28d4796a</id>
<content type='text'>
The view's name is now the RESOLVED display string and slug is untouched,
so machine formats carry both -- a script keeps the stable key, a human
reads the name.

name is a plain string, not a lang-keyed object. That removes the
shadowing hazard outright: a dotted {{name.la}} used to fall back
WHOLESALE to the enclosing month's name.la and print Ianuarius on every
unnamed day, which is how a printed booklet came to show the month where
the feast belonged.

weekday, season, rank and colour all gain localised companions, because a
calendar in a language needs more than feast names, and templates gain a
term vocabulary so fixed strings need no template edit to translate.

Asserted over a whole year: no day renders its slug as its name.

Beyond the brief's own code sample:

- comm_value's own name is now resolved through the same lang.celebration
  table too (not only the observed day's), because Task 8's own ordo
  template interpolates a plain {{name}} inside {{#comms}} -- an Obj there
  would render silently blank. A commemoration slug without Latin
  coverage still degrades to the slug, same as everywhere else in this
  system; that is a lang/la.ini DATA gap (113 of 327 sanctoral slugs,
  measured), not a regression this task introduced.

- bin/main.ml's emit/table/publish call sites needed ~lang to compile at
  all, which is collateral from the of_days signature change, not this
  task's own file list. Rather than pass Lang.raw and ship the very
  slug-as-name defect this branch exists to fix, they load the shipped
  Latin table by the same probe order data_dir() already uses -- a
  deliberate, commented BRIDGE that Task 6 replaces wholesale with real
  --lang/--raw/config resolution. bin/dune gained colitur_naming
  accordingly.

- test/cli.t needed two related fixes to stay green: the CSV header/row
  example, and a table/LaTeX escaping demonstration that relied on the
  kernel's own English name for Sts Peter &amp; Paul -- gone from the view
  now that name resolves through lang tables only, and the Missal's own
  Latin spells the feast with et, never an ampersand. Escaping itself is
  still proved live on 2035 data in test_emit.ml.

- Both schemas gained the new day/week/top-level keys (season_name,
  weekday, rank_name, colour_name, term, weekday_headings, month_num,
  month_name), not only the name shape change; schema/colitur-v1.xsd
  verified against real emitted XML via xmllint (make check-schema).

Render/golden's 9 cases (the shipped ordo/grid templates, all six
flavours) now fail as expected: their old {{name.la}} / {{#name}}...
idiom finds nothing on a plain string. That is Tasks 8/9's own scope to
rewrite, per the plan's own pre-flight conflict scan -- not fixed here,
and not silently pinned by regenerating goldens off broken output.

495 tests run (490 + 5 new), 486 pass; the 9 failures are exactly
Render/golden's ordo/grid cases.
</content>
</entry>
<entry>
<title>fix(render): omit DTEND at the domain's own last day, 9999-12-31</title>
<updated>2026-08-19T07:26:17Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-19T07:26:17Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=d71504ad9fc39b735689da32fa0be8a63c2cc7f8'/>
<id>urn:sha1:d71504ad9fc39b735689da32fa0be8a63c2cc7f8</id>
<content type='text'>
F1: Date.add_days is UNBOUNDED (date.mli) -- only Date.make enforces
1583..9999 -- and Date.to_iso8601 pads but never truncates, so
9999-12-31's naive successor formatted as "10000-01-01", and compact
turned that into a 9-digit, non-conformant DATE on the last VEVENT of
year 9999. Confirmed at the source before fixing, and reproduced
against real `colitur emit --format ics --from 9999 --to 9999` output
(DTEND;VALUE=DATE:100000101) before touching any code.

RFC 5545 section 3.6.1: a VEVENT with a DATE-valued DTSTART and
neither DTEND nor DURATION has an implicit one-day duration, so
omitting DTEND for that one event is the standard's own correct
answer, not a workaround. dtend_of re-derives the successor's
year/month/day and re-validates them through Date.make -- the one
function that actually enforces the domain -- before trusting the
string; None means the caller omits the DTEND line entirely.

F2 (minor, same function): documented next_day's own Error branch as
dead-but-silent on shipped data (event's iso &lt;&gt; "" guard is the only
caller and always parses) -- behaviour unchanged, comment only.

Two new tests: the domain's last VEVENT (DTSTART 99991231) has no
DTEND line at all; every DTEND anywhere in a 9999 feed is exactly 8
digits (the general form of the bug, catches a regression anywhere
else in the domain too). Existing 2027/2028 DTEND-arithmetic
assertions untouched and still pass.

Mutation-proved: both new tests fail against the pre-fix code
(9-digit DTEND value caught verbatim), pass after.
</content>
</entry>
<entry>
<title>feat(render): iCalendar emitter, RFC 5545</title>
<updated>2026-08-19T07:09:36Z</updated>
<author>
<name>Lukasz Kasprzak</name>
<email>lukas@labunix.xyz</email>
</author>
<published>2026-08-19T07:09:36Z</published>
<link rel='alternate' type='text/html' href='https://git.labunix.xyz/colitur.git/commit/?id=6abc243e5d40bd60174a3fb1affc9517b0ac5617'/>
<id>urn:sha1:6abc243e5d40bd60174a3fb1affc9517b0ac5617</id>
<content type='text'>
Not a template job: folding, escaping, exclusive DTEND and stable UIDs
are rules a logic-less template cannot enforce, and each fails silently
in a subscriber's client rather than loudly at generation.

  DTEND is EXCLUSIVE for an all-day event (section 3.6.1). Wrong here
  shows every event a day short, everywhere.
  UIDs are YYYYMMDD-&lt;rite&gt;@colitur and stable across regenerations
  (section 3.8.4.7). Wrong here duplicates the whole year in every
  subscriber's phone, months later.
  Every line is CRLF-terminated and folded at 75 octets (section 3.1).
  No RRULE: a liturgical calendar is not a recurrence rule. Asserted, so
  nobody optimises it later.

DTSTAMP is a parameter, not a clock read. RFC 5545 requires it and the
obvious implementation reads the wall clock -- which violates the
kernel's determinism rule and would make two feeds from identical data
differ byte-for-byte, defeating reproducible builds and any reviewable
diff on a published tree.

Corrected one test literal against real engine output: DTSTAMP is a
per-VEVENT property (section 3.8.7.2), not calendar-level, so the
default-value line count is 365 (every event), not 1.

Mutation-tested: a non-exclusive DTEND reddens the suite.
</content>
</entry>
</feed>
