aboutsummaryrefslogtreecommitdiff
path: root/lib/render/emit_ics.ml
Commit message (Collapse)AuthorAgeFilesLines
* fix(render): derive citation keys from the day, not a hardcoded pair (W5)Lukasz Kasprzak2026-08-271-0/+6
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 <citation part="second"> 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.
* feat(cli): --lang and --raw, colitur lang, colitur configLukasz Kasprzak2026-08-201-2/+11
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 > config > 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.
* feat(render): names reach the view and every emitterLukasz Kasprzak2026-08-191-8/+4
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 & 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.
* fix(render): omit DTEND at the domain's own last day, 9999-12-31Lukasz Kasprzak2026-08-191-2/+42
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 <> "" 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.
* feat(render): iCalendar emitter, RFC 5545Lukasz Kasprzak2026-08-191-0/+67
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-<rite>@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.