aboutsummaryrefslogtreecommitdiff
Commit message (Collapse)AuthorAgeFilesLines
* fix(render): omit DTEND at the domain's own last day, 9999-12-31Lukasz Kasprzak2026-08-192-3/+86
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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(cli): colitur emit -- csv, json, sexp, xml, icsLukasz Kasprzak2026-08-194-21/+359
| | | | | | | | | | | | | | | Reuses resolved_year_report's existing two-liturgical-year indexing rather than copying it: that walk owns the civil-vs-liturgical span reasoning, and a second copy would drift. It is refactored to return the days, with the printer layered on top, so day and readings behave identically -- which cli.t proves byte-for-byte. CSV emits one header for a whole multi-year run, not one per year. A reversed range is a usage error rather than silently empty output. Asserted in cli.t: two ics runs are byte-identical, because nothing in the path reads a clock.
* feat(render): iCalendar emitter, RFC 5545Lukasz Kasprzak2026-08-194-1/+184
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* feat(render): XML emitter and schemaLukasz Kasprzak2026-08-196-2/+151
| | | | | | | | | | | | | Element-per-field; attributes carry identity only and there is no mixed content, so a consumer's XPath never has to distinguish the two. Schema validation is an opt-in make check-schema via xmllint, not an in-suite assertion: validating XSD needs an XML library and the dependency list is frozen. It prints SKIPPED loudly when xmllint is absent, because a silent skip reads as a pass. The suite asserts well-formedness properties directly instead. This corrects the design spec, which claimed in-test validation.
* fix(render): remove rank_label -- it duplicated name verbatimLukasz Kasprzak2026-08-191-2/+6
| | | | | | | | | | | | | | | | | | view.ml's rank_label field was a byte-for-byte copy of the celebration's name (names_value cel.Celebration.names), not a localized rank label at all -- the kernel has no per-language rank names to draw one from, so there was no honest value to put there. Nothing consumed it: no template in the plan, no test, no other code referenced it. Removed from both day_value and padding_cell so the two key sets stay identical (23 keys each, verified). schema/day-v1.json already described 23 keys and needed no change -- it now matches the emitted output exactly. schema/day-v1.json is a published contract: once a phone subscribes or a site fetches this, removing a field is a breaking /v2/ change. The time to remove a field that lies about its own contents is before anyone can depend on it, not after.
* feat(render): CSV and JSON emitters, and the published contractLukasz Kasprzak2026-08-197-1/+262
| | | | | | | | | | | | | | | | | | | | | Both consume the VIEW, not the kernel, so every emitter and every template describe exactly the same fields -- there is one vocabulary, not five. CSV is RFC 4180: a field with a comma is quoted. That is live on real data, not hypothetical -- 'St. Joseph, Spouse of the Bl. Virgin Mary' would otherwise split into two columns. JSON is hand-rolled because the dependency list is frozen and escaping is the only subtlety. Control characters below 0x20 are \u-escaped per RFC 8259 section 7. There are no numbers in the view, deliberately: a consumer never has to guess whether week is 2 or "2". schema/day-v1.json pins the shape. Once a phone subscribes or a site fetches this, it is a promise to strangers -- adding a field is minor, renaming one means /v2/.
* feat(render): the view modelLukasz Kasprzak2026-08-195-1/+355
| | | | | | | | | | | | | | | | | | Shapes a civil year of resolved days into the value a template renders against. This layer is why the engine can stay logic-less: a month grid needs leading blank cells, week bucketing and an in-month test, and a logic-less template can compute none of it. Both weeks and days are offered at every level -- the booklet walks days, the grid walks weeks -- so the two artefacts cannot drift. Colours are six booleans, not hex: hex bakes a presentation policy into the engine, and LaTeX, groff and HTML each want a different colour expression. Asserted: exactly one of the six is true on every day of a whole year, so a template keying off them can never get none or two. Padding cells carry every field a real day carries, empty, so a template never hits a missing key mid-grid.
* feat(render): template renderer with mandatory escapingLukasz Kasprzak2026-08-194-1/+145
| | | | | | | | | | | | | | | | Every interpolated value is escaped for the template's flavour; the template's own literal text never is, because that is the author's markup. There is no raw form, so a template cannot opt out. Scope is a stack with outward fallback, so a grid template can reach the year number from inside a week without the view duplicating it into every cell. A missing key renders empty -- the one deliberate silence, so a template survives a rite that does not set every optional field. Mutation-tested: dropping the Escape.apply call reddens the data-cannot-escape-flavour case.
* fix(render): reject empty tag paths, sharpen the raw-form testLukasz Kasprzak2026-08-192-7/+42
| | | | | | | | | | | | | | | | | | F1: test_no_raw_or_partial_form's first assertion only excluded one literal shape (Ok [Var ["{name"]]), so it could not actually catch a future raw/unescaped constructor under a different name. Replace it with an assertion of the real parse result for {{{name}}} (Ok [Var ["{name"]; Text "}"]), documented behaviour rather than a guarantee this test cannot check -- the real guarantee is structural: node has exactly four constructors and none of them is raw. F2: {{.}}, {{#}}, {{^}} and {{/}} used to parse to a Var/Section/ Inverted with an empty path, reachable but never designed. This engine has no "current context" for a bare dot to mean, so a bare-dot or empty-sigil path is now a parse error at lex time, covering all four sigil forms via one path helper. The existing "empty tag {{}}" branch is unchanged and still reachable (a fully empty body is a distinct case from a sigil with an empty path).
* feat(render): logic-less template parserLukasz Kasprzak2026-08-194-1/+177
| | | | | | | | | | | Placeholders, sections, inverted sections, comments. Nothing else: no partials, no lambdas, no expression evaluation, no raw form. A template is data, never a program, which is what keeps an untrusted template safe. Errors rather than silence on a malformed template: an unterminated tag, an unclosed section, a mismatched close and a partial all return Error. Swallowing '{{name' as text is how a typo becomes invisible missing output in a printed booklet.
* fix(render): make fold_ics total on arbitrary octet stringsLukasz Kasprzak2026-08-192-8/+42
| | | | | | | | | | | | | | | | | | | fold_ics's UTF-8 backoff loop could back `cut` all the way down to `pos` on 74+ consecutive continuation bytes (0x80-0xBF), producing a zero-length chunk and recursing on the identical position forever -- not producible by valid UTF-8, whose longest continuation run is 3, but the kernel's own totality requirement covers arbitrary octet strings, not only valid ones. When backoff finds no boundary inside the window, cut hard at the limit instead, so forward progress is unconditional. test_fold_never_splits_utf8 previously asserted only that unfolding reproduced the original bytes, a property folding preserves at any cut position and therefore blind to a boundary violation. It now also asserts the named property directly: no continuation chunk may start with a UTF-8 continuation byte. A new regression test feeds fold_ics 100 consecutive continuation bytes and asserts it terminates with every line at or under 75 octets.
* feat(render): per-flavour escaping and RFC 5545 line foldingLukasz Kasprzak2026-08-196-2/+216
| | | | | | | | | | | | | | | Six flavours: latex, groff, html, xml, ics, none. Markdown, AsciiDoc and plain text map to none deliberately -- their metacharacters are context-dependent and escaping them aggressively produces worse output than not escaping. An unrecognised extension returns None rather than falling back to none: guessing the flavour wrong produces malformed output that looks fine until it does not. Folding backs off to a non-continuation byte, so a fold never splits a UTF-8 sequence -- the failure mode that would corrupt Polish and Latin names in a published feed.
* test(differential): C14 and C15 closed -- only slug vocabulary remainsLukasz Kasprzak2026-08-184-54/+59
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | lectio adopted the last two Missal corrections colitur argued: C14 3 -> 0 RG 96(a)/97/98. lectio resolved each impeded I-class feast's transfer independently, so in 2008, 2035 and 2046 St Joseph and the Annunciation both walked onto the Monday after Low Sunday and Joseph, losing there, was observed on no day of those years at all. It now resolves the year's transfers as a set: the Annunciation takes the Monday as its sedes propria, Joseph the Tuesday. C15 7 -> 0 The Holy Family propers' own 13-January rubric, "sine commemoratione Baptismatis D.N.I.C.". lectio kept the Baptism as the observed office on all seven such years in 2005-2050. Those were the last two BEHAVIOUR classes. What is left is C1 (138) and C6 (119), both pure slug vocabulary. Measured across the whole fixture, applying only the season normalisation the test already applies: 579 of 16801 days differ, and every one of them differs on the SLUG ALONE. Zero days differ on season, rank, colour, Epistle or Gospel. (The test's own norm_slug table already maps 322 of the 579; the 257 left are C1 and C6. The field profile is the same either way.) So the two engines now agree on every liturgically meaningful field on every day of 2005-2050. The residue is what the two projects call things -- ef-christmas-2-friday against ef-time-after-epiphany-1-friday for the same day, same Mass. Aligning it means renaming lectio's slugs, and those are keys: its lectionary, clectio's generated tables and any user overlay are built on them. Left as vocabulary rather than forced. Fixture refreshed against lectio e713da2; 13 rows changed, colitur agrees with all of them. 24 divergence classes at the start of this work, 2 now.
* test(differential): C31 closed -- 4 classes left, 257 of 267 rows are namingLukasz Kasprzak2026-08-184-131/+107
| | | | | | | | | | | | | | | | | | | | | | | | | | The ferias between Epiphany and the first Sunday after it repeat Epiphany's own Mass in lectio now; they had been taking the Mass of the Sunday that follows them. C31 98 -> 0 C1 40 -> 138 grew, and changed character C1 is now SLUG VOCABULARY ONLY, and its note says so. Season, rank, colour and both citations agree with colitur on every day of the January window; what differs is the identifier -- ef-christmas-2-thursday against ef-time-after-epiphany-1-thursday for the same day with the same Mass. C6 is the same shape. That is 257 of the 267 remaining rows. Aligning the vocabularies would mean renaming lectio's slugs, and those are keys: its own lectionary, clectio's generated tables and any user overlay are built on them. Recorded as vocabulary rather than closed, because the rows do differ -- just not in anything a reader of either engine's output sees. Measured for 2026: season, rank and colour agree on all 365 days; 14 days differ and every one differs on the slug alone. 5 classes -> 4, agreement 98.4%.
* test(differential): C20 and C38 closed -- agreement 98.4%Lukasz Kasprzak2026-08-184-305/+233
| | | | | | | | | | | | | | | | lectio carries the readings of the nine Common-routed saints and of three more that had proper Masses but no reading at all. Every day of 2005-2050 now resolves a reading there; four days used to come back empty. C38's own note said it could not close without lectio gaining a Commons concept. The readings arrived instead of the abstraction, which was enough: each Common resolves to one first/gospel pair for the saint that names it, so a table plus assignment layer would have bought indirection and nothing else. C20 14 -> 0 C38 70 -> 0 7 classes -> 5, 351 rows -> 267, agreement 97.9% -> 98.4%.
* test(differential): seven classes closed -- agreement 93.9% to 97.1%Lukasz Kasprzak2026-08-184-1005/+928
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | lectio gained the Passiontide week split and thirteen lectionary entries. C23 230 -> 0 Holy Week's own six Masses C32 99 -> 0 C35 60 -> 0 the first weeks after Epiphany and Pentecost (RG 299) C33 46 -> 0 Corpus Christi C34 46 -> 0 the Sacred Heart C26 43 -> 0 Passion Tuesday C19 7 -> 0 the Holy Family week-index residue C1 75 -> 40 C31 63 -> 98 GREW The Passiontide split is the one that carried most of this. efWeek had no Passiontide case, so both weeks numbered 0 and every day of Holy Week took the slug of its Passion-week namesake -- one set of readings for two weeks with entirely different Masses. Good Friday was reading Passion Friday's. C31 growing is the same bookkeeping as C1's earlier growth and is recorded in its own note: C32 and C35 closed, and rows that carried several causes now carry only this one. Nothing new disagrees. 17 classes -> 10, 1019 rows -> 488, agreement 93.9% -> 97.1%. What remains is almost entirely NAMING rather than disagreement: C6 (119) is colitur's ef-nativity-octave-day-N against lectio's ef-christmas-N-weekday on days where both now read the same Mass, and C1 (40) is the same shape in the January window.
* test(differential): C8 closed, C6 narrowed to namingLukasz Kasprzak2026-08-184-153/+155
| | | | | | | | | | | | | | | | | | | lectio gained Rogation Monday and Tuesday (RG 87, violet under RG 128(d)) and the Missal's own Mass for the weekdays within the Octave of the Nativity ("Diebus infra octavam Nativitatis Domini", Titus 3:4-7 / Luke 2:15-20). C8 26 -> 0 CLOSED C6 119 -> 119 narrowed: the cause halved, the count did not C6 is worth reading carefully rather than skimming the number. Its rows no longer differ on First_f or Gospel_f at all -- the two engines now read the SAME Mass on 29-31 December, where lectio previously served the Sunday's. What is left is Slug_f alone, colitur's ef-nativity-octave-day-N against lectio's ef-christmas-N-weekday. That is vocabulary, not disagreement. The count did not move because the entry's gate already admitted a Slug_f-only subset, which is exactly the case where a row count is a misleading summary. 17 classes, 1019 rows, agreement 93.9%.
* test(differential): C17 closed -- the largest divergence class is goneLukasz Kasprzak2026-08-184-1164/+1166
| | | | | | | | | | | | | | | | | | | | | | | | | | lectio built the votive Office of Our Lady on Saturday (RG 78) and then said RG 309(a)'s own Mass for it. Both halves were needed: RG 78 keeps the office and gives it white, RG 309(a) picks among the Missal's five seasonal formularies. Fixing only the first leaves the day announcing Our Lady and reading the feria's Mass -- the bug colitur itself shipped briefly and fixed in its v0.3.0. C17 439 -> 0 CLOSED -- the largest class between the two engines C31 103 -> 63 Saturdays that carried both causes; only this one left C35 69 -> 60 same C1 35 -> 75 GREW C1 growing is not a regression and is recorded as such in its own note: the January Saturdays C17 used to absorb now differ only on the slug vocabulary C1 already covers, so they land there instead. Rows moved between entries as the larger cause was removed. Also in this refresh, from the same session: weeks after Epiphany now count from the first Sunday after it rather than from 6 January, which took C19 from 35 to 7 -- what remains there is the Holy Family interaction, a different cause needing RG 17(b) in lectio. 19 classes -> 18, 1493 rows -> 1045, agreement 91.1% -> 93.8%.
* test(differential): refresh the fixture -- three more corrections adoptedLukasz Kasprzak2026-08-184-893/+891
| | | | | | | | | | | | | | | | | | | | | | | | | | | lectio has taken three further corrections colitur argued from the Missal: the twelve RG 124 colours (red is for Apostles, Evangelists and Martyrs, and missalemeum had the rule inverted in both directions), Good Friday's black under RG 128(b) with RG 132, and RG 72's Christmas Time boundary at 13 January inclusive. 864 rows changed. Two classes closed outright and two moved: C18 450 -> 0 the RG 124 colours -- the LARGEST single class in this file C36 46 -> 0 Good Friday's colour C1 159 -> 35 the season boundary stopped differing C23 184 -> 230 Good Friday's rows fold back, the split no longer needed C36 is closed rather than kept at zero because the split it represented has dissolved: it existed only to hold Good Friday apart while its diff set had a third member, and with the colour agreeing the rows are the exact pair C23 has always gated on. Restoring 230 is not a regression, it is the number returning to what it was before the split. Cited classes 24 -> 19, divergent rows 2252 -> 1521, agreement 86.6% -> 90.9%. The percentage is again the least interesting part. C18 alone was 450 days on which the two engines agreed about nothing and now agree because both apply RG 124 the same way round.
* docs(tools): the lectio patch exporter cannot see temporal correctionsLukasz Kasprzak2026-08-181-0/+11
| | | | | | | | | | | | It compares colitur's sanctoral data against lectio's calendar ini, so a correction to a temporal day -- computed in code on both sides, present in neither file -- is structurally invisible to it. Not hypothetical. The Good Friday colour fix (RG 128(b) + RG 132, v0.4.0) was missed entirely by this tool when the eight-field patch was produced, and surfaced only by running clectio at the bottom of the chain and diffing its output against colitur date by date. The tool narrows the search; it does not close it.
* docs: the lineage is broken at one link, and layer 4 is now the outlierLukasz Kasprzak2026-08-181-0/+25
| | | | | | | | | | | | | | | | | | | | | | | | | CLAUDE.md's lineage note said layers 3 and 4 are one chain -- Divinum Officium, missalemeum, lectio, colitur -- and that a defect inherited at the root is invisible to both. That is still true everywhere except on the days lectio has now corrected. lectio adopted eight corrections colitur argued from the Missal. On those days the chain no longer runs DO to lectio: lectio follows the Missal, and missalemeum is the only party still carrying the inherited error. C27, C28 and C37 went to zero and are closed; C38 narrowed to a readings-only divergence that cannot close without lectio gaining a Commons concept. The practical consequence for layer 4 is a base rate, and it is worth stating because it is easy to get backwards: on those days the oracle disagrees with BOTH engines, so M29 and M30 should be read as "missalemeum is the outlier" rather than "colitur is unusual". A fresh colitur-vs-missalemeum divergence is no longer presumptively colitur's fault. And the limit, stated so it is not over-read: everywhere else the two layers remain one lineage, and lectio's sanctoral is still generated from missalemeum by a script that would silently revert all four calendar corrections if re-run. That warning now lives in lectio's two ini headers and the generator's own doc comment as well as here.
* test(differential): refresh the lectio fixture -- the lineage invertedLukasz Kasprzak2026-08-184-254/+275
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Every previous refresh of this fixture carried lectio's answers TO colitur, because colitur's data was bootstrapped from lectio. This one carries colitur's answers BACK. lectio has merged eight field corrections colitur argued from the 1962 Missal: ubaldus and didacus promoted from commemoration to III-class feast on the Missal's own calendarium, both August vigils recoloured violet under RG 128, and both Ember Saturdays' readings, which lectio had carrying St Thomas's and St Matthew's Masses respectively. 227 of the 16801 rows changed. Three cited classes went to zero and are CLOSED and REMOVED, citations preserved in the register, the same discipline C2-C5, C7, C9-C13, C21, C22 and C25 already record: C27 40 -> 0 the Advent Ember Saturday's readings C28 40 -> 0 the September Ember Saturday's readings C37 77 -> 0 the two vigil colours C37 is worth noting by name: its own note called the shared lineage "the sharpest demonstration in the project", colitur and lectio and missalemeum all carrying the same two wrong colours. Two of the three now carry the right one. C38 NARROWED rather than closed, and the distinction matters. Its identity half is gone -- lectio ranks both saints class-3 now -- but the same 70 rows still differ on readings, because both saints take their Mass from a Common and lectio has no Commons concept to resolve one. It is gated on diffs = [First_f; Gospel_f] exactly, which is C18's shape. It cannot close without lectio gaining a capability, which is a feature rather than a fix. Net: 24 cited classes to 21, 2252 divergent rows to 2095, agreement 86.6% to 87.5%. The number that matters is not the percentage though -- it is that those 227 days now agree BECAUSE BOTH ENGINES FOLLOW THE MISSAL, where before they agreed because both inherited the same errors from Divinum Officium. That is the failure mode CLAUDE.md's lineage note describes, resolved rather than documented, for the first time in this project.
* fix(examples): pin the three Polish classes from the source, one was wrongLukasz Kasprzak2026-08-181-14/+25
| | | | | | | | | | | | | | | | | | | | | | | | | | | The first version of poland.ini marked three classes INFERRED because the ordo's class column sits several lines below its own date row and the first extraction pass did not reach it. All three are now read from the source: Our Lady Queen of Poland class-1 (line 1114), St Stanislaus class-1 (1233), Our Lady of Czestochowa class-1 (2398). One of the three inferences was WRONG. Czestochowa was written class-2 as "the conservative reading"; the ordo gives class-1. The mistake was treating the ordo's "part." marker -- particular, proper to certain places rather than universal in Poland -- as implying a lower rank. Those are different axes. The extraction method was validated before being trusted, against two entries whose class was already known from a clean single-line row: St Adalbert class-1 and St Andrew Bobola class-2. It reproduced both. Visible consequence, and it is correct: at class-1 Czestochowa no longer commemorates St Zephyrinus, because RG 111(a) admits no ordinary commemoration on a I-class day. The ordo also names the particular sees each patron is proper to -- Stanislaus for four archdioceses and three dioceses, Queen of Poland for Czestochowa and Przemysl and the military ordinariate -- which is now recorded beside them, and which reinforces the header's existing warning that a diocesan calendar is not a national one.
* release: v0.7.0v0.7.0Lukasz Kasprzak2026-08-183-3/+3
|
* docs: CHANGELOG entry for 0.7.0Lukasz Kasprzak2026-08-181-0/+3
|
* feat(examples): ship two real local calendars, with their limits statedLukasz Kasprzak2026-08-186-1/+330
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Poland and the Benedictines, in the flat INI form, installed beside the invented diocesan example. poland.ini -- 17 entries from the Calendarium Perpetuum pro Dioecesium Poloniae (1964), promulgated under Rubricarum instructum and applying to the 1962 Missal. Transcribed from a published Polish EF ordo that names that same calendar, cross-checked against missalemeum's supplement page for the two formularies 1964 added (13 and 15 July). Date, Latin name and class were read from the source for every entry; three classes could not be recovered from the PDF's column layout and are marked INFERRED where they appear, with the reasoning. The header carries an edition warning that is a real trap here: the Proprium Poloniae of 1921 and 1934 is still bound into many missals and carries an outdated arrangement of dioceses and ranks. It is not this calendar, and it is exactly the kind of plausible wrong-edition source that has cost this project time before. benedictine.ini -- two entries, and the comment explaining why is the point of shipping it. Counted across the Norcia ordo: 86 "I cl.", 43 "II cl.", ZERO "III cl." and ZERO "IV cl.", against 103 "Semidup." and ~96 "Dup.". The monastic rite uses the Roman classes at the top and the older Duplex / Semiduplex grades below, exactly where the Roman calendar has III and IV class. colitur's rank vocabulary cannot express those, so most Benedictine propers cannot be written here at all -- a mismatch of vocabularies between related rites, not a gap in the data. Rather than invent a Duplex -> Class3 mapping the source never states, that file ships only what its ordo gives in Roman terms and lists roughly two dozen excluded feasts BY NAME, so the omission is visible instead of silent. Both headers say plainly that they are examples and not authorities: they are transcriptions from published ordines, none of the five test layers can vouch for either, and both should be checked against the reader's own ordo. The cram test asserts only what we control -- that they parse, convert and apply -- and says so. They also demonstrate the precedence engine on real data: the Benedictine Transitus is I class and takes 21 March with the Lenten feria commemorated, and Maurus is II class and takes 15 January with Paul the First Hermit commemorated.
* docs(man): explain how an overlay is actually appliedLukasz Kasprzak2026-08-181-0/+65
| | | | | | | | | | | | | | | | | | | | | | | | | The format was documented; the pipeline was not. The commonest surprise when writing a local calendar is 'my feast does not appear', and it is almost never a loading failure -- which means the existing docs answered a question nobody was asking. Four stages, named: load, merge, resolve, emit. The one that matters is resolve, and the point it makes explicit is that overlay entries take part in precedence on EQUAL TERMS. A local feast is not privileged for being local; it competes under RG 91's table exactly as a universal one does. Three concrete causes of a missing feast, each of which came up while testing this branch: outranked by the day it lands on, a date structurally occupied (an easter+60 feast can never appear, because Corpus Christi is Easter+60 and is I class), or a commemoration past RG 111's limit for the day's class. And the diagnostic rule -- if check says the file loaded and every directive found its target, the answer is stage 3, so raise the class or move the date. Also records why the shipped adjustments are applied first and why that is not configurable: replacing rather than layering would silently drop the inseparable Peter/Paul commemoration, the Major Litanies, St Barbara and Rogation Wednesday. Overriding one deliberately by naming its slug is a different thing from losing it by accident.
* merge: a flat INI overlay front endLukasz Kasprzak2026-08-188-1/+674
|\ | | | | | | | | A convenience format for simple local calendars, transpiled to the existing S-expression form and verified against it before emitting.
| * feat(overlay): a flat INI front end, which verifies its own outputLukasz Kasprzak2026-08-188-1/+674
|/ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 -> sexp -> 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.
* docs(man): colitur-overlay(5), the overlay format in fullLukasz Kasprzak2026-08-184-6/+277
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The overlay format was documented in three partial places -- a paragraph in colitur(1), a block in --help, and the comments inside the shipped example -- none of which was a reference. Someone writing a diocesan calendar had to read all three and infer the rest. Section 5 because an overlay is a thing a user AUTHORS rather than a command they run: it belongs beside fstab(5), not in man1. Covers every directive and every field edit, the six required fields and the two optional ones, all three date specifications including the signed Easter_offset and the negative nth, three worked examples, and the caveats. The subject field gets a note explaining that it is not decoration -- it decides whether a feast displaces an occurring Sunday under RG 16(a). Two things it says that the code says and the old prose did not. There is no Set_status and no Set_date among the field edits, deliberately: changing an entry's status or its date makes it a different celebration rather than an edited one, so Replace is the right directive and the change stays visible in `colitur check` output. And a local feast missing from output has usually LOST its day under the general rubrics rather than failed to load -- the engine applies precedence to overlay entries exactly as to universal ones, which is the first thing an author hits and was written down nowhere. Writing it caught a documentation bug before it shipped: a first draft listed a Set_status edit that does not exist and omitted Remove_name that does. Every documented edit is now cross-checked against overlay.mli. Linked from colitur(1)'s SEE ALSO and its OVERLAYS section, and from --help. The Makefile installs it into man5, removes it on uninstall, and the man and doc targets lint both pages.
* release: v0.6.0v0.6.0Lukasz Kasprzak2026-08-183-3/+3
|
* docs: CHANGELOG entry for 0.6.0Lukasz Kasprzak2026-08-181-0/+3
|
* docs: point --help and the man page at the new overlay workflowLukasz Kasprzak2026-08-182-3/+60
| | | | | | | | | | | | | Both listed check and new-overlay among the commands but neither told a reader how they fit together, which is the part that makes them useful. The overlay sections now carry the four-step loop -- new-overlay, edit, check, run -- and state what check does not do, since its name invites a stronger reading than it earns. Also documents what the format now permits: citations and layer optional, the signed Easter_offset, the negative nth counting from the end of the month, and the legal values of each of the six required fields, which previously appeared only in the shipped example.
* merge: overlay authoring ergonomicsLukasz Kasprzak2026-08-185-13/+356
|\ | | | | | | | | | | Two mandatory fields made optional, parse errors that stop naming kernel source files, and a feedback loop -- colitur check and colitur new-overlay -- for a file the test layers deliberately cannot vouch for.
| * feat(cli): colitur check and colitur new-overlayLukasz Kasprzak2026-08-183-10/+210
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Writing a local calendar had no feedback loop. An overlay is applied, not validated -- that stays true, and the five test layers still cannot vouch for a user's file -- but before this the only way to learn whether yours did what you meant was to generate a year of output and grep for your own slug, and the only way to see that a directive matched nothing was to notice a warning scroll past among 365 lines. `check` loads each overlay, applies it to the real shipped calendar, and reports the directive counts, the slug each one targets, and any directive that found no target. It exits 2 when a file fails to load or a directive matched nothing, so it composes into a Makefile or a pre-commit hook rather than merely being readable. It is applied to the SHIPPED calendar and not to an empty layer on purpose: against an empty one every Suppress would fail trivially and the check would be worthless. It answers three narrow questions -- does the file parse, does every directive find its target, what does the merged result contain. It does not validate a calendar against the rubrics and cannot, and both the help text and the man page say so rather than letting the name imply more than it does. `new-overlay` prints a starter to stdout for redirection, rather than writing a file where it likes. Every value in it is a placeholder that will appear in `day` output if left unedited, so a half-finished overlay is visible rather than silently inert, and it documents the three date shapes and the legal values for each closed field inline. load_ef_layer now returns its diagnostics instead of printing them: day and readings still want them on stderr beside a year of output, while check wants them on stdout, attributed to the overlay that produced them, and counted. Printing at the source made the second impossible. The cram test round-trips new-overlay through check rather than pinning the template line by line -- editing its prose should not fail a test, but a syntax error in it still must.
| * feat(overlay): default citations and layer, humanise parse errorsLukasz Kasprzak2026-08-182-3/+146
|/ | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* feat(tools): export the lectio EF correction patchLukasz Kasprzak2026-08-182-1/+92
| | | | | | | | | | | | | | | | | | | colitur has been downstream of lectio since its data was bootstrapped from it. These are the fields where that relationship should now invert: eight changes, each annotated with the colitur allow-list entry carrying its rubric. Deliberately a PATCH and not a regeneration. colitur carries 205 of the 327 Polish names lectio ships, so regenerating lectio's ini from colitur would silently drop 122 of them; colitur also uses a different slug vocabulary for Passion/Holy week and different Paschaltide week numbering. What colitur is authoritative for is the adjudicated fields, and only those are emitted. Comparing the two datasets field by field also caught a counting error in colitur's own register that no assertion in this project was watching for: six sanctoral entries carry subject = Lord, not the four recorded, because the regex that counted them truncated on the two longest Polish names. The RG 112(b) reachability measurement was re-run against all six and is unchanged at 0/0; both docs are corrected.
* docs: record why RG 112(b)/(c)/(d-saint) are unbuiltLukasz Kasprzak2026-08-181-2/+12
| | | | | | | | | | | | | | | | | | | | | | | They have been carried as a bare 'unbuilt' for weeks. The reason is not neglect, and it differs per clause -- now measured across 1583-9999 rather than asserted. (c) cannot fire in this architecture at all: Temporal_ef.temporal returns one office per day, so two de Tempore candidates never coexist. Nothing to build. (b) is already produced by RG 16(a), which is implemented: 0 days in either direction, because a Lord feast takes an occurring Sunday's place with no commemoration, and no Class1 Sunday shares a date with any of the four Lord-subject entries. (d)'s saint half has no candidate pair. Three apparent pairs are distinct saints sharing a forename -- recorded so the false positive is not rediscovered -- and the one real pair, agnes and agnes-secundo, is 7 days apart and co-occurs 0 times. (b) and (d) are data-unreachable rather than architecture-unreachable, so an overlay can make either live. That is the entry_14_movable_band shape. Left as an explicit open decision rather than silently resolved.
* docs: Rogation Wednesday is built, not blockedLukasz Kasprzak2026-08-181-11/+21
| | | | | | | | | | | | | | CLAUDE.md still described it as architecturally blocked, and that claim was being carried forward into task lists a release later. It was true when written and was superseded by the very next task: the blocker was that no (month, day) pair could anchor an Easter-relative trigger, and Date_spec gained Easter_offset in v0.2.0. The entity has shipped since then as an ordinary Add in adjustments.sexp -- 1 981 days across the domain carry it. Also recorded the general lesson, since this cost a stale item twice: a blocker phrased as an architectural impossibility was really a claim about one type's expressiveness, and it dissolved when that type grew a constructor.
* release: v0.5.0v0.5.0Lukasz Kasprzak2026-08-183-3/+3
|
* docs: CHANGELOG entry for 0.5.0Lukasz Kasprzak2026-08-181-0/+3
|
* merge: sanctoral status audit and RG 113's first sentenceLukasz Kasprzak2026-08-1810-12/+199
|\ | | | | | | | | | | Two corrections argued from the Missal's own calendarium and rubrics, both found by consulting primary sources the project already had but had not fully read.
| * fix(ef): the seasonal commemoration comes first (RG 113 sentence one)Lukasz Kasprzak2026-08-182-1/+68
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | "113. Commemoratio de Tempore fit primo loco. In admittendis et ordinandis aliis commemorationibus, servetur ordo tabellae praecedentiae." Only the second sentence was implemented, adopted on ef-rg16a as the admission and ordering criterion. The first -- the commemoration OF THE SEASON is made in the first place -- was not, so ordering ran through band alone for every commemoration including the seasonal one. Reconciled with RG 110(c), which adds the inseparable Peter/Paul pair "ante omnes alias commemorationes". Both rubrics are primary-source certain: the photographic scans' OCR is illegible at RG 110(c), but docs/research/LT.txt, the electronic transcription, carries the Rubricae Generales complete, and O'Connell's footnote 39 turns out to render that clause exactly rather than gloss it. They share a technical term. RG 113's own sentence fixes the sense of "aliae commemorationes" as the ones other than the de Tempore one it has just placed primo loco, and RG 110(c) sits three paragraphs earlier in the same code. Read consistently, the pair heads the OTHERS, not the whole list: season, then the pair, then the table order. So this runs after rg110_additions, and List.partition's stability keeps the pair adjacent while the seasonal commemoration steps in front of it. Blast radius, two full 1583-9999 sweeps diffed: 3 533 days, every one ORDER-ONLY -- the commemoration set is identical on both sides of every changed day, checked as sorted multisets, zero days where anything else moved. All are 22 February with chair-of-st-peter observed. Exactly the population measured before the rule was written. The order-sensitive test row is in test_precedence_ef.ml's admit_cases, which is still the only place in the suite that asserts commemoration order at all. It needed a mixed-origin candidate: every other candidate in that table is origin = Temporal by default, so the rule is an identity on them.
| * fix(ef): ubaldus and didacus are III-class feasts, not commemorationsLukasz Kasprzak2026-08-188-11/+131
|/ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Both carried rank Class3 with status Commemoration_only, bootstrapped from lectio, which inherits it from missalemeum, which is generated from Divinum Officium. The Missal's own universal calendarium ranks both "III classis" outright, with no commemoration rubric: "S. Ubaldi Ep. et Conf., III classis" (16 May) and "S. Didaci Conf., III classis" (13 November), corroborated in the second scan and in a published 1962 Ordo. Universal by construction, not by assumption: the calendarium carries no "pro aliquibus locis" marker in its 573 lines while the wider Missal carries 79 of them elsewhere. Each saint also has his own Mass entry in the Proprium, which a bare commemoration never has -- a commemoration carries an oration only. Found by auditing ALL 290 fixed-date entries against that calendarium, day by day, anchored on the Roman calends column because the arabic day column is OCR-wrecked and the dominical-letter column vanishes in some months. These two were the only status defects in the whole file. The 15 days the automated pass could not cover were read by hand and all agree. Neither needed a proper authored: each takes its Mass from a Common with only its own Oratio proper, per its own rubric at its own date. ubaldus shares patrick's Common exactly. didacus needed one newly transcribed -- common-of-a-confessor-not-a-bishop-2, the "Altera Missa" Iustus, 1 Cor 4:9-14 and Luke 12:32-34, read independently in both scans, which agree, so the page-image third reading this file's method requires on disagreement was not triggered. Blast radius, full 1583-9999 sweep, every day classified: 13 036. That is 5 564 ubaldus observed, 7 196 didacus observed, and 276 where the Ascension Vigil still wins on 16 May but its single RG 111(c) slot passes from rogation-wednesday to ubaldus -- correct and already pinned, since the Rogation days are the MINOR Litanies and so an ordinary commemoration, which RG 113's table order ranks below a III-class feast. Cited as C38 (70 rows) and M30 (3); C8, C17 and M18 shrink accordingly, each with the reason recorded rather than the count silently adjusted.
* release: v0.4.0v0.4.0Lukasz Kasprzak2026-08-183-3/+3
|
* docs: CHANGELOG entry for 0.4.0Lukasz Kasprzak2026-08-181-0/+3
|
* merge: EF rubrical corrections from O'Connell and a published 1962 OrdoLukasz Kasprzak2026-08-187-6/+132
|\ | | | | | | | | Three colour fixes, each argued from the Missal and each invisible to every validation layer that shares colitur's own data lineage.
| * fix(ef): the Assumption and St Lawrence vigils are violet (RG 128)Lukasz Kasprzak2026-08-186-3/+83
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | RG 128, transcribed in the rules register since 2026-08-11, gives violet to "vigils of II and III class outside Paschaltide". The Vigil of the Assumption (14 August, II class) carried White and the Vigil of St Lawrence (9 August, III class) carried Red. Both are outside Paschaltide, so both are violet. The Ascension's vigil is untouched: it is the one II-class vigil inside Paschaltide and was already correctly white. The rule had been sitting in the register, disagreeing with the shipped data, since before the data was bootstrapped. It was invisible because colitur, lectio and missalemeum all carried the same two wrong colours -- colitur was bootstrapped from lectio, lectio's ini is generated from missalemeum, and missalemeum uses Divinum Officium's data files. Three sources agreeing is one source counted three times, and the differential and the oracle both went green on the shared error. It surfaced only against witnesses outside that tree: a published 1962 Ordo flagged the Assumption's, and O'Connell, The Celebration of Mass 4th ed. (1964), section 4(c) and footnote 169, gave the general rule and with it St Lawrence's, which the Ordo omits entirely. Two golden pins added, in years where each vigil is actually observed rather than impeded. The divergence this creates against every DO-lineage source is cited in both allow-lists: C37 (lectio, 77 rows -- 92 possible minus 15 where the vigil falls on a Sunday) and M29 (missalemeum, 3 rows in 2026-2027 and 2 in each of 2035 and 2038). Both gated on the exact slugs or dates and on colour alone, so any other divergence on those days would still surface. Also recorded, no code change: rose on Gaudete and Laetare stays. RG 131 is permissive ("adhiberi possunt") and the Ordo prints violet, so the colour field carries a permission rather than a requirement on those two Sundays a year -- a decided position now, not an unexamined one.
| * fix(ef): Good Friday is black, not violet (RG 128(b), RG 132)Lukasz Kasprzak2026-08-184-3/+49
|/ | | | | | | | | | | | | | | | | | | | | | | | | | | RG 128(b)'s own exception list, transcribed in the rules register and primary-source-verified since Task 16, excepts "Actione liturgica feria VI in Passione et Morte Domini usque ad Communionem exclusive" from the violet that otherwise runs from Septuagesima to the Easter Vigil. RG 132 assigns black there. The register recorded this as an acknowledged gap rather than a disputed reading -- the code comment beside the Holy Thursday exception says so in as many words -- and it is closed here. Found by two witnesses outside the Divinum Officium lineage that colitur, lectio and missalemeum all share: O'Connell, The Celebration of Mass, 4th ed. (1964), revised throughout to the Codex Rubricarum (1960) and the 1962 Missal, and a published 1962 Ordo. Both say black. So does missalemeum's own colour set, which orders the day "bv". The rubric is per-action ("usque ad Communionem exclusive") and the model emits one colour per day; black is the day's principal one, the same acknowledged limit RG 126's Palm Sunday blessing already carries. Blast radius: one day per year, colour only, across the whole 1583-9999 domain. Two golden pins re-pinned. Good Friday's 46 differential rows leave C23 for a new C36 gated on the exact triple [First_f; Gospel_f; Colour_f], so C23's own exact-pair gate still means what its note says for its remaining four days: 184 + 46 = 230, no residue, both counts from the comparator rather than hand-counted.
* docs: name the lineage problem, and record an independent Ordo witnessLukasz Kasprzak2026-08-181-0/+15
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Two research findings, and the first changes how every earlier adjudication should be read. missalemeum's own repository states it uses Divinum Officium's data files via a git submodule. lectio's ini is generated from missalemeum. colitur was bootstrapped from lectio. So the chain is Divinum Officium -> missalemeum -> lectio -> colitur, and the other projects in this space are the same tree: OfficiumDivinum is an object-oriented rewrite of Divinum Officium, Breviarium Meum is based on its texts. That is worse than the caveat already recorded. CLAUDE.md described layer 3 as sharing colitur's lineage and layer 4 as a separate oracle. It is not separate: layer 4 is layer 3's own upstream. The two oracles are one source seen at two removes, so every colitur-versus-missalemeum adjudication is in substance colitur versus Divinum Officium -- the de facto standard for this whole software space. That makes the six prior adjudications more consequential, and it means the project has one external software witness plus the scans, not two. The second finding is a witness outside that tree entirely: the published Ordo, compiled by clergy applying the rubrics. The 2025-26 Ordo for the Traditional Latin Mass shows commemorations AND distinguishes privileged from ordinary, which is precisely colitur's thinnest axis. Its liturgical timeline matches colitur exactly, and it confirms seven colitur positions -- including M16, reversed against Divinum Officium two days ago on RG 111(d), and the Nativity Octave work from this morning, both halves, including the RG 69 Sunday guard. Three of the confirmations matter most: the Major Litanies, the RG 110 30-June Peter companion, and St Barbara are entries colitur hand-authored because they are missing from lectio AND missalemeum, that is from Divinum Officium. The Ordo carries all three, and even notes the Peter commemoration is said "w/ 1 conclusion" -- RG 110's own pro unica habeantur. Those were the least-supported entries in the dataset and are now the best-corroborated. The one apparent disagreement resolves in colitur's favour: the Ordo lists St Evaristus as 26 October's Mass where colitur observes the feria and commemorates him, but the calendarium reads "Commemoratio S. Evaristi... Comm." in both sources, and the Ordo's own preamble explains it lists Masses that may be said on a class 4 feria. An Ordo is a practical document, not a rank authority. Not wired in as a layer: the PDF's columns shift between pages, automated extraction recovered 271 of 364 rows and truncated text mid-word, and a fragile layer that silently mis-parses is worse than none. A structured Ordo would be the only way to add a genuinely independent fourth lineage. Register: section 6.20.