aboutsummaryrefslogtreecommitdiff
path: root/test
Commit message (Collapse)AuthorAgeFilesLines
* feat(cli): colitur rubrics prints the day's Mass formularyLukasz Kasprzak2026-08-221-4/+89
| | | | | | | | | | | | | | | | | | | | | | Separate from day for the reason readings is: a formulary name contains spaces and day's row is space-separated with a variable-length tail. Modelled on readings' own structure (year parsing, --overlay plumbing, error handling): --overlay is accepted, since an overlay can change which celebration is observed and hence which Mass is said. --lang, --raw and --sigla-* are refused rather than silently ignored -- this row resolves no display name and no citation for any of them to affect. Output is TAB-separated (date, formulary slug, source), not space or " | " like the two existing reports: a resolved formulary NAME is a column a later task may add, and it can carry both spaces and punctuation a citation never does, ruling out either separator already in use. day and readings verified byte-identical (sha256) against a v0.10.1 build across 1583/1900/2026/2038/9999. Man page documents rubrics beside readings and renders through groff with no errors.
* docs(ef): retract the false LMS Ordo witness claim for Mass_formulary.VotiveLukasz Kasprzak2026-08-221-4/+10
| | | | | | | | | | | | | | | | | | | | | | | | | Fix round 1's own comment claimed the Latin Mass Society Ordo witnesses the Votive tag directly, reading its "V Mass of BVM" as an abbreviation for "votive". That was wrong: the Ordo's I-V are roman numerals naming WHICH of the Missal's five seasonal "Missae de sancta Maria in sabbato" (RG 309(a)'s own "iuxta temporum diversitatem") is said on a given Saturday, not a marker of the Mass's kind. All five numerals occur through the Ordo (I x1, II x4, III x3, IV x3, V x8); the false reading came from "V Mass of BVM" also being a substring of "IV Mass of BVM". Votive itself is unaffected and stays correct on RG 431(e) alone, quoted verbatim in its place: "e) in Missis votivis IV classis de Angelis, quocumque die, et de B. Maria Virg. quae in sabbato celebrantur" -- votive Masses of the IV class, of the BVM, celebrated on Saturday. The retraction is recorded, not softened, in lectionary_ef.ml/.mli and the test pin's own comment, together with what the Ordo's numeral actually means so the mistake is not repeated, and the consequence it leaves behind: because the numeral names WHICH seasonal Mass is said, a future comparison against the Ordo (Task 6) can validate bvm_saturday_citations' own season-keyed selection, not merely that some BVM Mass was chosen. Comments and docs only -- no behaviour change. Both suites re-run green and colitur day re-confirmed byte-identical.
* fix(kernel,ef): a votive Mass is not the day's own -- add Mass_formulary.VotiveLukasz Kasprzak2026-08-212-5/+19
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Mass_formulary.source was missing a case for a Mass said IN PLACE of the day's own office's Mass while that office is itself kept unchanged -- RG 309(a) and RG 431(e) both classify the RG 78 Saturday Mass of Our Lady this way, in the Missal's own words, as a "Missa votiva IV classis... de B. Maria Virg.", not as the day's own office's Mass. The Latin Mass Society Ordo (docs/research/ordo/lms-ordo-2024-2025.pdf) witnesses it directly, printing that day as "V Mass of BVM". The BVM-Saturday branch in Lectionary_ef.readings was tagged Own_slug for lack of a better constructor when Task 2 landed, flagged there as a judgement call rather than a specified answer. That call was wrong: left as Own_slug, a future comparison against the LMS Ordo (a later task in this plan) would read every BVM Saturday as a manufactured divergence between colitur's "own" Mass and the Ordo's votive one. Retagged to Votive; said is unchanged (still the day's own, reused ferial, temporal slug) since the office itself is unaffected, only the Mass said for it. Added the new constructor's source_to_string case ("votive") and its own test row, and pinned the branch itself in test_lectionary_ef.ml's formulary cases at 1 August 2026, verified directly against the resolver rather than assumed. Both this session's own drifted pins that the earlier commit inherited from the task brief are unaffected by this change, and both are re-confirmed independently correct in this round: dune test and the exhaustive sweep are green, and colitur day stays byte-identical against the pre-fix-round binary.
* feat(kernel,ef): the lectionary reports which Mass it saidLukasz Kasprzak2026-08-214-17/+152
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Rite.readings now returns (Mass_formulary.t option * Citation.t list) instead of a bare citation list, and Liturgical_day.t carries the result as a new formulary field. Validate holds a rite that resolves a formulary at all to resolving one on every day, the same discipline it already applies to citations; the EF lectionary chain resolves Some on every day of every year 1583-9999, confirmed by a direct sweep over 2005-2050 as well as through Validate itself. Plan Tasks 2 and 3 are merged into this one commit on the coordinator's own instruction: Rite.readings' signature and the field that consumes it are one atomic edit, and the intermediate state does not compile on its own. Each of the four lectionary steps now builds its own Mass_formulary.t at the point it decides, not by re-deriving it afterwards from the citations it returns: step 1 tags Proper with the observed slug, step 2 tags Own_slug with the day's own temporal slug, step 3 tags Preceding_sunday with the resumed Sunday's temporal slug, and step 4 tags Common with the Common's own id -- Commons.find now returns that id alongside its citations rather than discarding it, since it is only ever in scope at the point the assignment is looked up. The RG 309(a) Saturday votive Mass of Our Lady, which answers between steps 4 and 2 rather than as one of the four numbered steps, is tagged Own_slug too: Mass_formulary.source has no dedicated constructor for it, and its own guard only ever fires when the observed celebration already is the day's own (reused ferial) temporal slug, which is exactly what Own_slug documents. Recorded as a judgement call in the task report, not a specified answer. test/cli.t's `emit --format sexp` line count is repinned (8472 to 8881): that command serializes Liturgical_day.t whole, so the new field grows its output. `colitur day` itself is untouched -- verified byte-identical against the pre-change binary across 1583, 1900, 2026, 2038 and 9999.
* feat(kernel): a type for which Mass a day saysLukasz Kasprzak2026-08-212-1/+31
| | | | | | | The lectionary's four-step chain already decides whether a day says its own proper, its own slug's entry, the preceding Sunday's Mass or a Common, and then discards that decision once the citations are out. An ordo needs to print it.
* docs(C39): day-level confirmation from the LMS OrdoLukasz Kasprzak2026-08-211-7/+11
| | | | | | | | | | | | | | | | | | | | | | | | The Latin Mass Society Ordo 2024-2025 for England and Wales (v2.21, compiled by Peter Day-Milne) gives 9 August 2025 as "OUR LADY on SATURDAY IV Cl W / V Mass of BVM / Commem of St Romanus M at Low Mass" -- no vigil -- and 10 August as the 9th Sunday after Pentecost with St Laurence commemorated. That is colitur's output on both days, field for field. It is discriminating where the Ordo already in docs/research was not. This edition lists six vigils as the day's office across its year, including the Assumption's on 14 August 2025, and its footnotes discuss arranging a diocesan feast around the Vigil of St Laurence in other years. Its absence on 9 August 2025 is an application of the rule, not a data gap. The extraordinaryform.org Ordo omits that vigil on every date it covers, including 2027-08-09 where the clause says to keep it, which is why it witnesses nothing here. Also confirms the Vigil of the Assumption as II Cl violet, the RG 128 colour this project corrected from the bootstrap's White. The clause's other shape -- feast transferred away, 2022/2033/2038/2044 -- remains unwitnessed; no accessible Ordo covers those years. Stated in the note rather than left implied.
* docs(C39): withdraw the Ordo corroboration claimLukasz Kasprzak2026-08-211-4/+8
| | | | | | | | | | | | | | | | | | | | | | C39 and the 2025 golden pin both claimed the published Ordo independently confirms RG 33's third trigger, because it omits St Lawrence's vigil on 2025-08-09 where lectio and missalemeum keep it. It does not. The Ordo omits that vigil on 2027-08-09 as well, where the clause says to keep it (10 August is a Tuesday, the feast holds its day), and 2026-08-09 is a Sunday where the rule's first trigger omits it anyway. The 2024 date has no row. So the Ordo never shows St Lawrence's vigil on any date it covers, and the 2025 agreement is a blanket gap coinciding with the right answer, not evidence. The rule itself is unaffected: it rests on RG 33's text, which admits no latitude, and on lectio not evaluating the clause at all -- "RG 33" appears nowhere in its production code, and the third trigger cannot emerge from per-day rank comparison because it turns on what is observed the following day. The divergence is architectural, not a disagreement about the rubric. Recorded rather than deleted: the withdrawn claim is why this entry was first written up as unusually well corroborated.
* feat(ef): implement RG 33's third omission triggerLukasz Kasprzak2026-08-218-20/+199
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | RG 33 omits a II/III-class vigil in three cases: it falls on a Sunday, it falls on a I-class feast, "vel si festum cui praemittitur in alium diem transferri aut ad commemorationem reduci contingat". Only the first two were built; the third was recorded in precedence_ef.ml as unimplemented on the grounds that no witness existed in the shipped data. That reasoning was wrong, and the rule fires on 1 744 days across 1583-9999. Both halves of the clause reduce to one observable question -- is the feast the OBSERVED office on the following day (RG 34 puts it there) -- so the kernel asks it once per candidate, after place_transfers has settled the year. No fixed point is needed: a vigil is a candidate only on its own day, never on its feast's, so suppressing it cannot change what the next day observes. Precedence.rules gains vigil_feast, which returns the slug of the feast a vigil precedes; the kernel cannot infer that itself, because only two of the five vigil/feast pairs share a slug stem. Blast radius, measured pre-change binary vs HEAD over the whole domain and classified: 1 744 days, three shapes, zero unexplained. 1 199 are the feast reduced to a commemoration (10 August on a Sunday, St Lawrence); 478 and 67 are the feast transferred under RG 96 after the Sacred Heart or Corpus Christi takes its day. The Assumption's and the Ascension's vigils never qualify -- their I-class feasts always keep their own day. Independently witnessed, which is unusual here. The published Ordo -- the only witness outside the Divinum Officium -> missalemeum -> lectio lineage -- omits St Lawrence's vigil on 2025-08-09, agreeing with colitur against both engines. That date had been read earlier as an Ordo gap; the Ordo was right, and correcting the misreading is what surfaced this clause. On 2027-08-09 the feast does keep its day and the Ordo omits a vigil colitur correctly keeps, which is a genuine Ordo gap. Allow-lists: C39 (lectio, 10 rows) and a 2038 oracle class citing the register, the 2026-2027 window having no instance. The golden pin asserting St Lawrence's vigil is violet moved 2025 -> 2027; its own comment had reasoned about the vigil's weekday and missed that RG 33 also looks at the feast's. Two new pins cover both shapes of the clause. The vigil/feast table is built with Slug.of_string_exn: mutation testing showed that of_string plus Result.to_option turns a typo into None, which this hook's contract reads as "not a vigil", switching the rule off in silence. Two tests assert the table against the shipped data in both directions.
* feat(config): flavour is settable, and an INI overlay is diagnosedLukasz Kasprzak2026-08-212-0/+38
| | | | | | | | | | | The escaping flavour was the one rendering setting with no config key, so a template whose extension says nothing needed the flag every run. Unset still means infer, which config --show reports as (infer) rather than (none). Backfills the 0.8.0 and 0.9.0 changelog entries. Both were tagged by hand rather than through 'make release', which is precisely the guard that would have refused a release with no changelog line.
* fix(cli): diagnose an INI overlay passed to --overlayLukasz Kasprzak2026-08-211-0/+11
| | | | | | | | | | | | | | The INI overlay form is a SOURCE format that 'colitur convert' turns into the S-expression one. Handing the INI to --overlay instead failed deep in the sexp reader with 'more than one S-expression in file', which names neither the cause nor the cure -- and the shipped examples under data/ef/examples/ are exactly the files someone would try that with. The detector reads past the comment header to the first real line and looks for a [section]. Its first version capped that scan at 40 lines and poland.ini's own header is 43, so it silently never fired on the very file it was written for; the cap now bounds only how much of a binary gets read, and is not a guess at header length.
* fix(render): a month answers to both spellings of its own nameLukasz Kasprzak2026-08-201-0/+19
| | | | | | | | | | | | | | | | A week object carries month_name/month_num/month_abbr, because at that level a bare `name` would be ambiguous. An author who learned those names inside {{#weeks}} reaches for them one level up inside {{#months}} too -- where they resolved to nothing, and an unknown key renders as the empty string by design, so the result was a silently blank month heading rather than any error. Found by writing a template from scratch rather than copying a shipped one; every shipped template sidesteps it, which is why nothing caught it. The aliases are additive, so no existing template or golden changes. colitur-templates(5) now documents both spellings and says why they exist.
* fix: audit findings — parser strictness, name ambiguity, and errorsLukasz Kasprzak2026-08-203-5/+142
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Found by auditing the shipped program rather than the diff. The parser accepted OCaml integer-literal syntax, so "Luke 1_1:5" read as chapter ELEVEN and "+5" as 5 -- a typo silently becoming a different chapter, reachable through any user overlay. Numbers are now plain digits and positive, and a descending range is rejected: 1:20-10 is always a transcription error. No shipped citation changed. FOUR PAIRS OF DIFFERENT BOOKS SHARED A FULL TITLE. 1 and 2 Corinthians both rendered "Epistola ad Corinthios", as did Thessalonians, Timothy and Peter -- 108 citations in 2027 alone that a reader cannot resolve to a book. This is the Kings defect fixed earlier and not generalised. The titles now carry their volume numeral, marked CONSTRUCTED, and a test asserts no two books share a name -- while allowing the case where two ids ARE the same book under different numbering, which a tradition relates. Spec section 8.5 is now delivered rather than merely recorded. Shipped styles did not re-parse their own output: 32 of 52 Latin abbreviations and 49 of 52 full titles failed, so a citation copied from colitur's own output into an overlay was passed through untouched and printed in the wrong language, silently. Every shipped name is registered as a spelling and split_book learned multi-word titles by longest-token match. Now 0 of 52 fail beyond the same-book aliases. Overlay errors were written for a compiler author: they named an OCaml source file the reader does not have and buried the useful token. The existing five-path rewriter is replaced by a generic one, applied to every load path rather than one, so "rank: is not one of the allowed values (at Class9)" replaces the raw Of_sexp_error dump. Also: the new-overlay scaffold documented citations and layer without showing them, and its comment implied the wrong nesting -- the single easiest thing to get wrong; error messages echoed whole file lines, copying an unrelated file's contents into stderr when a flag pointed at one; and config --show validated partway down its table, exiting 2 after writing five rows to stdout.
* fix(citation): close the final review's blocking findingsLukasz Kasprzak2026-08-202-14/+41
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The branch was RED and reported green. `dune test` exited 1: test/cli.t pinned the pre-fix output `kings_1 19:3-8`, which the previous commit had already fixed to `1 Reg 19:3-8`. The gate command piped dune through `tail`, so it reported tail's exit status, and cram prints its diff BEFORE the alcotest summary, so the two lines shown were the passing ones. Verify with `dune test; echo $?`, never through a pipe. A style file's own `book` key was unreachable. sigla_book resolved against a hardcoded "abbr" and the result was applied unconditionally, so the documented `[sigla] book = full` could never win. Render gains book_string, and the style's own value is now the default that a flag or config overrides. The unit test pinned style_of_fields correctly while the wiring defeated it. `lang --check` filtered the reference set to the celebration prefix, so a file with no [bible] section at all reported a clean bill of health -- contradicting both the reason the keys change was made and lang.ml's own comment. It now reports missing book names too. The token test missed a FOURTH citation-bearing file: adjustments.sexp writes citations as `Set_citation`, not `(reference ...)`. Its 16 citations all parse, so nothing was broken, but nothing was checking. The first attempt at this fix read the file and extracted NOTHING -- the marker stopped before the opening quote, so every payload was the part label -- which is recorded in the code rather than left as a trap. Also: colitur-config(5) claimed a trailing period the data does not carry, and two la.ini scan quotes silently corrected OCR damage ("Ionae 3, I - I O", "Epistolse") while presenting themselves as verbatim. Both are now marked as corrections.
* fix(lang): stop internal book ids reaching output, and finish the docsLukasz Kasprzak2026-08-201-0/+28
| | | | | | | | | | | | | | | | | | | | | | | | | | Three defects, all found by running the documented examples rather than by reading code. All seven modern-numbering targets carried their own internal id as their Latin name (kings_1.full = kings_1), so --sigla-tradition modern printed "kings_1 19:3-8" -- leaking a key book.mli states is never shown to a reader. They now carry Vulgate-derived Latin names marked CONSTRUCTED: the 1962 Missal uses Vulgate numbering throughout, so it can contain no incipit for a book that exists only under a later convention. In Latin the modern tradition therefore only moves Kings and Esdras; Osee, Ionas, Ecclesiasticus and the Apocalypse keep their names either way, since modern numbering is a vernacular convention. The coverage test could not catch this. It compares a name against its KEY ("kings_1.full"), and "kings_1" differs from that, so the row passed. A second check now asserts no name IS its own id. The two are complementary: one catches a missing row, the other a row present but filled with the wrong thing. Mutation-proved. --help still claimed sigla settings were resolved "but not yet rendered", stale since the render wiring landed. colitur-config.5 documented none of the three settings even though config --show prints them.
* feat(lang): Latin and English book names, and the shipped sigla stylesLukasz Kasprzak2026-08-2013-175/+205
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | la.ini and en.ini both gain [sigla] (the current Vulgate/Latin punctuation convention, byte-identical to Render.default_style) and [bible] (a .full and .abbr row for every id in Book.all -- 45 cited ids plus the 7 tradition targets, 52 total). Shipping [sigla] changes no output, asserted by the full test run. Shipping [bible] does change rendered book names, which is the point. la.ini's titles are sourced from docs/research/scan1.txt/scan2.txt (the 1962 Missal scans), each row citing the line its incipit pattern was read from. Six pairs (kings_3/4, corinthians_1/2, thessalonians_1/2, timothy_1/2, peter_1/2) share one Missal incipit and differ only in the sourced .abbr, matching what the primary text itself does. Three ids (proverbs, song_of_songs, ecclesiasticus) and the seven tradition targets are marked UNSOURCED and fall back to the data's own spelling, per this project's central rule against inventing a Latin title. Re-running the task's own sourcing note against the scans, counting every hit rather than eyeballing a frequency-sorted list, found nine of its NOT-sourced verdicts were undercounted (a single clean hit, buried under higher-frequency matches): Galatians, Colossians, both Thessalonians, both Peter, Malachi, Numbers, Jonas, Osee and Esdras all have a clean incipit in the scans and are sourced here. The note's other three verdicts stand, confirmed independently. Ecclesiasticus is not simply unfound: this Missal reuses Wisdom's own Lectio libri Sapientiae incipit for Ecclesiasticus readings too (both were anciently classed as one Sapiential group), so using it for Ecclesiasticus would misidentify the book, not merely abbreviate it -- recorded in that row's own comment. en.ini's [bible] is filled in full, not left partial the way [celebration] is: without it, a Vulgate-numbered id would fall through the [meta] fallback chain to la.ini's Latin name, not merely a less complete English one. Traditional Douay-Rheims names for the Vulgate ids (3 Kings, Osee, Ecclesiasticus, Isaias, Apocalypse), modern names for the seven tradition targets, since --sigla-tradition modern is the reader asking for modern numbering. test_lang_coverage.ml gains test_every_book_named, asserting every Book.all id has both forms in la.ini -- the check that catches a forgotten tradition target, since nothing else in the suite ever names them. Shipping real book names changes 11 golden template renders (Render/golden) and several test/cli.t examples that used to demonstrate the pre-Task-10 default-spelling fallback; both are updated to the new, correct output, each line checked against a fresh render before promoting.
* feat(render): citations render through Sigla at both call sitesLukasz Kasprzak2026-08-2014-1509/+1629
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | View.citation_ref and readings' part_ref are the only two places a citation reaches output; every emitter goes through View. --raw passes Sigla.verbatim rather than a style built over Lang.raw: an identity name table would still reformat punctuation and renumber. Loosen reject_sigla_for: --sigla-style/--sigla-book/--sigla-tradition now actually render on readings/table/render/emit/publish (each builds its own Sigla.t via the new load_sigla), and stay refused only on commands that render no citation (day, easter, temporal, ...). names_of degrades a Lang.bible miss to Book.default_spelling rather than printing the lookup key itself ("luke.abbr"); pinned in cli.t against a language file with no [bible] section at all, independent of la.ini/en.ini's own eventual [bible] section. Regenerate the 11 golden templates for 2027: every changed line is one canonical citation replacing a stored variant, verified against the full readings 2027 diff (raw vs default) cell by cell -- 86 distinct (raw, rendered) pairs account for the entire diff across all 11 files, with zero unexplained residue. Two families: the seven duplicate book spellings collapsing onto one canonical form (Isa./Isa, 3 Kgs./3 Kings, ...), and citations reconstructed from parsed structure dropping stray punctuation the parser already treats as noise (a trailing period or semicolon, a comma chapter/verse separator, an elided inherited chapter) -- each of the latter already named and tested in test_citation.ml's parse_suite before this task.
* feat(config): sigla_style, sigla_book and sigla_traditionLukasz Kasprzak2026-08-202-13/+121
| | | | | | | | | Same flag > config > default precedence as --lang, and each reported by config --show with its source, so an override is visible rather than mysterious. A language file's [sigla] section IS a style; a config key SELECTS one and may override settings within it -- the two are not a duplicate setting.
* fix(citation): complete the modern tradition, and test the shipped fileLukasz Kasprzak2026-08-201-4/+40
| | | | | | | | | | | | | | | | | | Apocalypse -> Revelation was missing from [modern] while `revelation` was already a declared tradition target, so the target existed with nothing mapping onto it. Same Vulgate-to-modern shape as Ecclesiasticus -> Sirach. The injectivity test used to hand-copy the mapping table, which asserted things about the copy and nothing about the artifact: a mapping added to traditions.ini and forgotten in the test would have passed. It now reads lang/traditions.ini and additionally asserts that [vulgate] is empty -- an entry there would silently renumber the DEFAULT, the one thing this design promises never happens -- and that no declared target is left unreachable. Mutation-proved three ways: declaring an unused target, deleting a mapping, and adding an entry to [vulgate] each redden it, the first two naming the offending book.
* feat(lang): traditions.ini, and its install ruleLukasz Kasprzak2026-08-202-2/+25
| | | | | | | | | | | | | | | | Which book a reference denotes does not vary by language, so it lives in its own file rather than in la.ini beside [weekday]. Six mappings, not four: Osee/Jonas are the same question as 3 Kings in transliteration form. The install rule is added in the same commit deliberately -- lang/ once shipped without one, so the feature worked from the source tree and was broken once installed. bin/dune gains colitur_citation as a linked library, needed for the new loader; lang_list's directory scan now excludes traditions.ini, which is not a language file and does not parse as one.
* feat(lang): read [bible] and [sigla] sectionsLukasz Kasprzak2026-08-201-1/+47
| | | | | | | | | | | | | | | | | | Lang reads a hardcoded list of section names and ignores anything else silently, so without this a [bible] section would appear to work and do nothing -- confirmed directly with a scratch executable: before this change, of_string on text containing a [bible] section parsed without error and Lang.keys came back empty, no trace of the section anywhere. Lang.bible keeps the total-lookup contract every other lookup in this module has: a miss returns the key itself, never the empty string. Lang.sigla_fields returns the [sigla] section's raw fields, values trimmed but still quoted, for Render.style_of_fields to unquote. [bible] joins the keys reference set so lang --check reports missing book names; [sigla] deliberately does not, being settings with working defaults rather than translatable names -- adding it would make --check demand five settings from every language file.
* feat(citation): the Sigla facade, total by constructionLukasz Kasprzak2026-08-203-24/+92
| | | | | | | | | | | | | | | | | | format never raises and returns an unparseable citation unchanged, so a gap degrades to today's behaviour rather than to a crash. The coverage test asserts separately that no shipped citation takes that path. verbatim is what --raw uses: an identity name table would still reformat punctuation and renumber, which would break byte-exact diffing against lectio. The existing citation-coverage walk (1970-2070) now also drives a round-trip check in the same pass: parse -> render -> parse must reach the same structure, using Book.default_spelling for names (Book.to_string returns the internal id, which is not a registered token and cannot be read back). Proved with a mutation: changing the default style's part_sep to a separator the parser does not accept reddened the round-trip case on 31 multi-part citations and nothing else; reverted.
* feat(citation): default_spelling, and close two test gapsLukasz Kasprzak2026-08-201-0/+48
| | | | | | | | | | | | | | | | | | | | | | | | Book.default_spelling returns the first registered spelling for an id. It is the fallback display name, and it exists because the alternative is worse: a language file's [bible] lookup is total and returns THE KEY on a miss, so a book with no entry would render as "luke.abbr 5:12-14". Falling back to the data's own spelling makes it render as "Luke 5:12-14" instead -- what colitur printed before this feature existed. The degraded case is the old behaviour, the same principle Lang states for its own key-returning misses. Two test gaps closed, both found by mutation rather than by reading: Parse's split_book scans a leading ordinal digit over '1'..'4', and no case in the suite used an ordinal above 1. Narrowing the range to '1'..'3' passed every test while seven real citations depend on it ("3 Kings 17:8-16", "4 Kings 5:1-15"). Parse-layer cases added; the first attempt at this test asserted through Book.of_token, which is a table lookup and never reaches split_book at all. default_spelling is asserted to round-trip: every cited id's fallback spelling must itself resolve back to that id, or Render and Parse disagree the moment a book goes unnamed.
* feat(citation): render a parsed citation in a configurable styleLukasz Kasprzak2026-08-202-0/+112
| | | | | | | | | A style is a set of format strings, so punctuation convention is data. Values are unquoted here rather than in Overlay_ini: that parser trims every value and is shared with overlays and [defaults], so teaching it about quotes would change behaviour this feature has no business changing.
* test(citation): assert every emitted citation parsesLukasz Kasprzak2026-08-202-0/+44
| | | | | | | | | Walks 1970-2070 and parses every citation the engine emits. Nothing validated citation spellings before this, which is why seven books carried two spellings unnoticed. Mutation-proved: removing the 'Isa.' spelling from the book table reddens this test naming that citation.
* feat(citation): parse citations into structureLukasz Kasprzak2026-08-202-0/+55
| | | | | | | | | The parsed form is a book and a LIST of chapter-parts: the data cites across chapters and lists disjoint verse ranges within one. Two rules the shipped data forces and that are not obvious: a semicolon-separated part may inherit the previous chapter rather than restate it, and a chapter may be separated from its verses by a comma.
* fix(citation): survey all three citation-bearing files, not just the lectionaryLukasz Kasprzak2026-08-201-4/+109
| | | | | | | | | | | | | | | | | | | | | The book table was built against data/ef/lectionary.sexp alone. That undercounts: sanctoral.sexp carries more citations than the lectionary and, together with commons.sexp, cites 21 book tokens the table was missing entirely (62 distinct tokens across all three files, not 42), several of them common (2 Tim appears 8+ times). Add the missing spellings to existing ids (2 Cor, Col., Exod, Ezek, James, Sir, Eccli) and ten new ids for books not cited before (1-2 Timothy, 2 Peter, Apocalypse, Judith, Malachi, Proverbs, Song of Songs, Tobit, Wisdom). Sir and Rev are modern spellings sitting inside Vulgate data, so both resolve to their Vulgate ids (ecclesiasticus, apocalypse) rather than to the sirach/revelation tradition targets -- mapping them to a second id would double-map the same book. Add a duplicate-spelling invariant test (List.assoc_opt would silently prefer the first match on a collision) and a test that re-derives the token set from all three data files at test time and asserts every token resolves, rather than trusting a survey performed once by hand.
* feat(citation): canonical book ids and tradition mappingLukasz Kasprzak2026-08-203-1/+45
| | | | | | | | | Seven books arrive in two spellings, inherited from lectio's ini and ultimately from Divinum Officium. Collapse them onto one id here rather than editing generated data. Naming and renumbering are kept apart: a tradition decides which book an id denotes, a language file decides what it is called.
* docs(templates): fix groff Build: lines to match check-templatesLukasz Kasprzak2026-08-202-2/+2
| | | | | | | | | | | | | | | | | | | | | ordo.ms's own Build: comment omitted -P-pa5 entirely, so the documented command produced A5-sized content on a physical A4 PDF page (595x842pt instead of 419x595pt) -- verified by running the command verbatim and reading pdfinfo. Both .ms Build: lines were also missing -Kutf8 and -t, which the Makefile's check-templates target already carries: without -Kutf8, accented Latin names (e.g. Peñafort) render as mojibake, confirmed by extracting text from the rendered PDF. Bring both templates/ef/ordo.ms and templates/ef/grid.ms in line with the Makefile's actual invocation (-ms -t -Tpdf -Kutf8 <papersize>), so a user copying the Build: line gets the same result as 'make check-templates'. Updated the two golden fixtures that pin these templates' rendered output byte-for-byte to match. Verified every documented build command (README's two pdflatex invocations, both .ms lines, both .typ lines) by running it verbatim and checking pdfinfo/pdftotext output; no other flavour's Build: line or man page example was affected.
* feat(templates): retune ordo.tex to ordo.typ's density, fix ordo.ms's ownLukasz Kasprzak2026-08-202-4238/+4032
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | ordo.tex was 53 pages / 6.9 days-per-page on 2027 against ordo.typ's 27 pages / 13.5 -- the user prefers the tighter Typst booklet. Read ordo.typ's own typographic decisions (base size, the day-number/name/detail-line size hierarchy, box padding, inter-box skip, leading) and matched them: an explicit 9pt/10.5 base (was \normalsize, 10pt), the day number and feast name both 9pt bold (bold distinguishes them, not size, as ordo.typ decided), 7pt weekday, 6.5pt detail lines, sharp-cornered boxes, and needspace amounts re-measured for the shorter boxes. Result: 29 pages / 12.6 days-per-page -- within the "a page or two of 27" target, reached without sacrificing legibility (verified against the rendered PDF, not assumed). The one real gotcha: \fontsize set inside a brace group that closes before the line-ending \\ has no effect on the interline glue TeX inserts for that break -- the glue uses the OUTER scope's \baselineskip, not the just-closed group's. This is exactly the shape of bug the pre-retune box already had (every \fontsize group closed before its own \par), which is why the first retune pass barely moved the page count. Fixed by declaring \fontsize/ \selectfont/\bfseries/\mdseries inline, ungrouped, immediately before the text each governs. Recorded in the file's own header for next time. ordo.ms looked comparatively fine (43 pages) but was rendering on A4, not A5 -- roughly double an A5 page's area, so normalised for area it was already looser than ordo.tex's own pre-retune baseline. Fixed, each found by rendering and reading the real PDF rather than assumed: added -P-pa5 to the Makefile's groff invocation (mirroring grid.ms's existing -P-pa4l); fixed the day box reading the RAW rank/colour strings (class-2, green) instead of the resolved Latin pair (rank_name/colour_name) -- exactly the "printed slugs" defect the ordo rebuild's own commit message named, which this file had regressed to; added -Kutf8 (groff's default input encoding produced visible mojibake on an accented Latin name); tightened fonts/ leading the same way as ordo.tex; found and fixed a stray-blank-line bug present since the file's original authoring (every day/week/month boundary left an accidental blank template line, read by groff as its own extra .sp); found and fixed a genuine print-safety bug (text sitting 0.13mm from the true page edge -- .pl, groff's text page length, is independent of -P-pa5's device paper selection and needed matching explicitly before ms's own FM-based bottom margin would fire at all); and added a .ne guard so a week header can no longer be stranded alone at a page's foot. Net: 43 pages (A4, broken) to 39 pages (real A5, print-safe, real Latin names, correct encoding, zero orphans) -- no numeric target set for this flavour, but no lower number was reachable without leaving one of the above unfixed. ordo.html and every grid.* flavour are untouched, as scoped. Both golden/ordo-2027.{tex,ms} regenerated through the real test harness path (Test_support's loaders + Test_view's en-with-la-fallback view_of), not the CLI, whose own --lang default (la) differs from the harness's and would have pinned content the suite never produces. Full suite green: 503 tests (504 with the exhaustive sweep). make check-templates/check-schema/check-citations all clean.
* feat(templates): week headers stop repeating the month nameLukasz Kasprzak2026-08-207-522/+553
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Every ordo booklet week header used to print the month name again even though the month heading right above it already established it -- pure noise, repeated once per week for the whole year. All seven ordo flavours now print the week's own Roman numeral plus the span of dates it covers instead, e.g. "Hebdomada I (Ian 1-2)", with no month name on the line; the LaTeX table of contents and the Typst outline (built automatically from its own headings, so editing the heading is the whole fix there) read the same way. A single-day week (e.g. a lone trailing Sunday) renders "Ian 31", not "Ian 31-31": each flavour's dash sits inside an inverted single_day section, so the engine -- which cannot itself compare first_dom to last_dom -- never has to decide anything, it only follows the data. The dash character matches each flavour's own existing convention: LaTeX/Typst's own "--" ligature, groff's \(en escape (alongside its existing \(bu), HTML's &ndash; entity (alongside its existing &middot;), and a literal en dash for the three flavours with no escaping (adoc/md/txt). Every in-template comment describing the new fields is written without ever typing two curly braces together, including in the two comments this touches that used to embed a live {{month_name}}/{{term.week}} tag pair inside a LaTeX % comment -- harmless only because that whole line already started with %, not because the engine has any comment awareness (it has none). Goldens regenerated through test_render_golden.ml's own render path (Test_view.view_of + Template.render_string), not the CLI -- the two differ in default language and would otherwise pin output the test suite never produces. Grid goldens are untouched, matching the grid templates, which have no week header. Verified against real pdflatex (two passes) and typst compile output, not merely the golden pins: both flavours print "Hebdomada I (Ian 1-2)", "Hebdomada II (Ian 3-9)", "Hebdomada III (Ian 10-16)" as their first three week headers and identical text in the table of contents/outline, and both show "Hebdomada VI (Ian 31)" for January's own trailing single-day week with no dash.
* feat(render): Roman week numbers and date spans, as dataLukasz Kasprzak2026-08-203-0/+71
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The ordo booklet's week header repeated the month name on every week even though the heading above already established it -- pure noise. Replace it with a Roman week number plus the span of dates the week covers, e.g. "Hebdomada I (Ian 1-2)", following the project's own rule that a presentation choice is data, not code. lang/{la,en}.ini gain a [month_abbr] section (three-letter month abbreviations); Lang.month_abbr follows Lang.month's exact shape, including the out-of-range and miss-returns-the-key contracts. The coverage test now fails loudly if an abbreviation goes missing, the same as [month] already does. Every week object in the view gains num_roman (Roman numeral, num stays as the arabic original -- Roman is a presentation choice, not an engine change), first_dom/last_dom (the day-of-month of the week's first and last IN-MONTH days, padding excluded), month_abbr (resolved through Lang.month_abbr), and single_day (true when the week holds exactly one in-month day). single_day is a flag, not a preformatted span string: the engine is logic-less and cannot itself decide between "Ian 1" and "Ian 1-2", so a template makes that call from the flag instead -- the same "shape the data, not the template" discipline in_month and last already follow. Weeks are built per month with padding only at the two ends, so a week's in-month days never cross a month boundary -- verified, not assumed: every week always has at least one real day since no month is shorter than a single week. Covered by three new View tests, including a real single-day-week witness (January 2027's own trailing week is a lone Sunday, the 31st).
* fix(templates): the Typst grid no longer overflows a 6-week monthLukasz Kasprzak2026-08-201-4891/+2778
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | grid.typ used rows: (auto, 1fr) on the assumption that a Typst fractional row always shrinks to share whatever space is left. It does not: an 'fr' track's size is the LARGER of its fair share and its content's own minimum, so it grows PAST 1fr when content is taller than that share rather than compressing to fit. Adding the sigla line (Epistle/Gospel references) grew every cell's natural content past what a 1/6-of-remaining-space row allows, and a 6-week month (three of twelve in 2027) split its own table between rows, stranding the last week alone on a fresh page with no heading -- the user's screenshot showed a row sliced mid-cell, 'I classis' cut off, the next month's own heading printed underneath it. Fixed the same way grid.tex's own \cellh already was: an explicit row height (cellh, computed from the page's text height divided by six, budgeted with 6mm of deliberate slack after a zero-slack first attempt still split a table between rows), with each cell's content wrapped in a height-clipped block sized to match -- content that still does not fit is truncated rather than spilling into the next cell or page. block(breakable: false) around the whole table is a second, independent backstop: even if the budget is ever wrong again, the WHOLE table jumps to a fresh page rather than splitting a row across two, a loud failure (an odd blank page) rather than a silent one (a sliced row). This reverts the file's own prior divergence from grid.tex (Typst stretching every month to fill the page regardless of row count, disclosed as deliberate at the time) -- overflow safety now matters more than that cosmetic fill. A 5-week month still leaves a blank sixth row, the same trade-off grid.tex's own header already accepts. Verified against all twelve months of 2027 (three 6-week: January, May, October; nine 5-week), typeset through typst compile: exactly 12 pages, one month title per page, no clipped or duplicated content, day 31 of a 6-week January fully visible. grid.tex itself re-checked for the same latent risk and found unaffected -- its fixed-height \cellh box already had enough margin. Golden regenerated through the same Test_view.view_of path the other affected templates already use.
* feat(templates): the ordo booklet flows continuously, not one week per pageLukasz Kasprzak2026-08-208-7686/+4536
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | One week per page filled roughly the top third of every A5 page and cost 65 pages for a year that needed far fewer -- the original requirement, reversed here: a full 7-day week already occupies most of an A5 page even at reduced type, so the real saving was always in the year's many SHORT boundary weeks (a month's first/last week is rarely a full seven days), not in packing two full weeks together. lib/render/view.ml gains a [first] field on every week object (true on a month's own first week, the same 'cheap flag beats invented template logic' call [last] already made for grid rows) -- there is still no {{../}} parent-path syntax for a template to test this any other way. ordo.tex: the per-week \clearpage is gone. Every week keeps its existing small header line; the month's own first week additionally prints a large, ruled banner. \needspace (a template-local LaTeX dependency, not one of the frozen OCaml deps) keeps a header from being stranded alone at a page's bottom -- and a real, separate bug surfaced building it: \label placed BEFORE \needspace captured the OLD page number when \needspace went on to force a break, producing a step-stale \pageref in the TOC (fixed by moving \label after both needspace checks). The day box itself is tightened (rank/colour folded onto the same line as the citations -- an A5 box is far wider than tall, so this fits even Holy Week's longest citations with room to spare) so a second week routinely shares a page with the first. Measured result: 65 -> 53 pages, two pdflatex passes still settle the TOC, zero orphaned headers, zero overfull boxes. ordo.typ: the per-week #pagebreak is gone; #show heading's own block gains sticky:true, Typst's built-in answer to what ordo.tex needs \needspace for. The month banner needed its OWN sticky:true, found live: without it a banner could sit alone at a page's bottom with the entire month pushed to the next page -- not caught by a check for an orphaned HEADING, since the banner is plain markup, not one. Measured result: 65 -> 27 pages (Typst's own page-fill packs tighter than ordo.tex's needspace-driven approach), one compile pass, zero orphans. The five non-paginated flavours (ms/html/md/adoc/txt) never had a forced page break to remove -- they mainly needed the same header treatment for structural consistency: a week-level header nested under each month's existing one, walking months>weeks>days (the week's own padded, in-month-guarded day list) in place of the old months>days flat loop. Verified against groff, asciidoctor and an HTML parser -- zero warnings. Goldens regenerated through the Test_view.view_of path test_render_golden.ml itself uses, not the CLI (whose --lang default would have pinned output the tests never produce).
* fix(naming): the rite prints its display name, not the internal id efLukasz Kasprzak2026-08-2011-13/+33
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The booklet's title page and running header read "Ordo 2027 . ef" -- an internal identifier reaching a reader, the same defect class the sanctoral/temporal slugs were already fixed for. lang/la.ini and lang/en.ini each gain a [rite] section mapping the rite id to a reader-facing name, sourced from the Missal's own title page (docs/research/LT.txt:6 "MISSALE ROMANUM", :15 "EDITIO TYPICA") plus the 1962 dating this project uses throughout (rules-register.md, CLAUDE.md) -- both cited in the ini comments, marked PATTERN since the phrase combines two title-page elements rather than quoting one verbatim heading. Lang.t gains a rite table/lookup (same total, miss-echoes-the-key contract as every other lookup here); View.of_days exposes rite_name alongside the existing rite field, which stays the stable key exactly as slug is kept beside name. Every shipped template that printed {{rite}} now prints {{rite_name}} instead. test_lang_coverage.ml gains a coverage assertion over every rite id the engine can emit -- one today -- so a second rite module (OF) landing without a matching [rite] entry fails loudly instead of printing its own bare id. Goldens regenerated through the Test_view.view_of path test_render_golden.ml itself uses (English with a Latin fallback), not the CLI (whose --lang default is plain Latin and would have pinned output the tests never produce). test/cli.t's own JSON prefix assertion updated to match: rite_name is a real new key in that generic view dump, sitting right after rite.
* fix(cli): close the flavour-list call sites Typst's addition exposedLukasz Kasprzak2026-08-201-3/+12
| | | | | | | | | | | | | | | | | | | | | | | | | | | The task this branch of work set out to test was whether the template engine is genuinely flavour-agnostic outside lib/render/escape.ml. It is not, quite: bin/main.ml's own --flavour error/help text and both man pages hand-typed the flavour list and its count ("the six flavours") as literal prose, so adding Typst as a seventh left three places silently wrong (an incomplete flavour list in two error messages, and "six" where the true count is now seven) until this commit. bin/main.ml's two error messages (unknown --flavour value, and cannot infer a flavour from an extension) now build their flavour list by walking Escape.all/to_string instead of a literal string, so the next flavour added will not reintroduce the same drift; the static --help body text and colitur(1)/colitur-templates(5) still name flavours and their count by hand; there was no equivalent gain available for prose formatted this specifically, so those were updated by hand instead, along with colitur-templates(5)'s FLAVOURS section, which gained typst's own subsection alongside latex/groff/html/xml/ics/none. test/cli.t's two pinned error-text assertions are updated to match, and gained a new case: .typ inferred from extension, and a real shipped citation's hyphen surviving --flavour typst's own escaping live, the same shape the 2035 Ss. Petri et Pauli case already proves for latex's ampersand just above it.
* feat(templates): ordo booklet and wall calendar in TypstLukasz Kasprzak2026-08-204-3/+15847
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Adds templates/ef/ordo.typ (A5, one week per page, each day framed with a colour swatch, real names, every fixed string through term.*) and templates/ef/grid.typ (A4 landscape, one month per page, 7 columns, colour-filled cells, Epistle/Gospel sigla), matching what ordo.tex and grid.tex already do. Typst resolves its own table of contents inside a single compile: no pageref/aux-file dance, unlike pdflatex's own two-pass need for ordo.tex. ordo.typ's TOC is one flat heading per week (Month . Week N) rather than ordo.tex's own month-once/weeks-nested layout, since the view model has no first-of-month flag for a template to test and inventing one would be new engine state for a cosmetic grouping. grid.typ deliberately diverges from grid.tex's own partial-fill behaviour for a 5-week month: Typst's row-size list repeats its own last entry for every remaining row, so a two-entry rows spec fills the page completely regardless of week count, where LaTeX's fixed cellh leaves a 5-week month's bottom sixth blank on purpose. Both divergences are documented in the templates' own headers. Two bugs found and fixed while building these against the real typst 0.14.2 binary: two stray hash characters in grid.typ's header comment that were not actually inside a line-comment (would have been parsed as Typst code, not prose); and two lines merging into one wherever they were separated only by a bare newline (still the same soft-wrapped paragraph in Typst, unlike LaTeX's own explicit paragraph break) -- fixed with a trailing backslash to force a real line break without adding inter-paragraph spacing, verified against a full-year render (zero typst warnings across all 365 days of 2027, in both templates). Goldens (test/golden/ordo-2027.typ, test/golden/grid-2027.typ) were generated through the harness's own View.of_days/Template.render_string path, via a throwaway generator, NOT through the CLI: the CLI's default language table differs from the harness's own English-chained-to-Latin table, confirmed live (diffing CLI output against the harness's own render showed exactly the expected language divergence and nothing else) -- the same trap this project has hit before. make check-templates now typesets both templates with the typst binary, using the same SKIPPED-and-exit-0 shape the pdflatex/groff blocks already use when their own tool is absent, verified in both directions: absent (PATH without typst) skips loudly and exits 0, and a deliberately corrupted template fails the target with a non-zero exit and typst's own error text.
* feat(render): Typst as a seventh escaping flavourLukasz Kasprzak2026-08-201-1/+61
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Adds Escape.Typst: type, all, to_string ("typst"), of_string, of_extension (.typ), and a per-character expand function, structured identically to the existing latex escaper (one pass, no re-scan, so double-escaping stays impossible by construction). The metacharacter set was verified against the installed typst 0.14.2 binary, not assumed: a probe document escaping each of #, *, _, $, @, <, >, `, \, ~ and - was compiled and pdftotext'd back to confirm the literal character survives, and each was separately confirmed to do something else when left bare (# opens code mode, */_ toggle strong/emph, $ opens math, @ opens a reference -- a bare unresolved @word is a hard compile error, not merely mangled output -- </> can close around a bare word into label syntax that swallows it whole, ` opens raw, ~ is a non-breaking space, and a run of two or three '-' becomes an en/em dash). All ten are backslash-escapable; none needed a non-backslash workaround. '-' is escaped unconditionally rather than only inside a detected run, since this escaper has no lookahead -- a probe confirmed escaping every hyphen independently still typesets as literal hyphens for a run of any length, so the single per-character rule is sufficient. test_escape.ml's new Typst cases were written first and shown to fail against a stubbed identity apply before the real escaper landed, per this project's own regression-test discipline. Comments in both files mark which new tests are genuine regression tests (the per-character escaping, including a real shipped citation and a synthetic markdown-habit overlay name) versus characterisation (to_string/ of_string/of_extension are flat table lookups with no logic to have been wrong).
* feat(cli): --lang and --raw, colitur lang, colitur configLukasz Kasprzak2026-08-201-26/+226
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* fix(templates): clear the wall calendar's class label of the border, add siglaLukasz Kasprzak2026-08-203-128/+924
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Problem 1: grid.tex's \daycell pushed the rank/class line to the exact bottom edge of its fixed-height parbox with \vfill, the same edge \hline draws the cell's bottom rule along, so 'I classis'/'IV classis'/'II classis' printed as if sliced by the rule. Fixed by nesting a second, shorter parbox (\cellh minus a new \cellpad, 1.5mm) inside the unchanged \cellh-tall outer one, so the \vfill only ever reaches to \cellpad above the rule, never touching it. \strut on both trailing lines belt-and-suspenders the fix for lines with no descenders. Problem 2: each cell now also carries the day's Epistle and Gospel references (view fields first/gospel), joined by a centred dot/bullet rather than repeated Ep./Ev. labels -- the same compact convention ordo.tex already uses between rank_name and colour_name. A grid cell is only ~3.5cm wide and Holy Week's own citations run 20-30 characters each, so class label and sigla get their own \tiny line apiece rather than being crammed onto one line and shrunk past readability; \cellh leaves about eleven \tiny lines of room, so two short lines cost little. Both first and gospel are wrapped in their own conditional section so a day missing either prints no stray separator (defensive: every day in the shipped EF lectionary has both, but nothing guarantees that of a future rite or overlay). grid.ms gains the same sigla, on their own .br line, using tbl's existing w() wrap rather than any manual line-splitting -- it was never subject to problem 1 (tbl auto-sizes row height, no fixed-height clipping risk) and never carried a class label to begin with, so none was added here. grid.html gains the sigla as a small, muted block-level span; unlike the two paper flavours it isn't boxed onto a fixed physical page, so the browser just grows the cell rather than needing any line-budget compromise. Verified with two pdflatex passes and groff -ms -t -Tpdf against all twelve 2027 months: zero Overfull warnings, 7 columns every row, class label and sigla both fully clear of every rule. test/golden/grid-2027.* regenerated through test_render_golden.ml's own render path (View.of_days + Template.render_string, the EN-with-LA-fallback lang table), not the CLI, per this project's own standing trap about the two tables differing.
* test(golden): refresh ordo-2027 goldens for the newly named commemorationsLukasz Kasprzak2026-08-196-570/+570
| | | | | | | | | | | | | | | | | | | | | | The six ordo booklet goldens (tex, ms, html, adoc, md, txt) were stale, not broken: the commemoration-naming task landed 120 Latin names for sanctoral slugs, so the render path test_render_golden.ml exercises now prints real names (e.g. 'St. Telesphorus Pope and Martyr', or 'S. Felicis' via en.ini's own la.ini fallback where English has no entry) where the committed goldens still pinned the raw slug ('telesphorus-pope-and-martyr', 'felicis'). Regenerated through the same path test_render_golden.ml uses -- en.ini chained to la.ini via Test_view.view_of, never the CLI's Latin-only default -- with temporary env-gated scaffolding in the test file, used once and reverted before this commit. Every changed line is a 1:1 slug-to-name substitution; file structure, line counts, and everything else are untouched (570 insertions / 570 deletions across the six files). Verified before committing: no unrendered {{ }} tags, none of the 332 known sanctoral/adjustment slugs leak into any of the six files, all twelve month headings and all 365 day entries are present in each. Full suite (495 tests), check-templates, and check-citations all still pass.
* feat(lang): walk commemorations and transfers, name every slug they surfaceLukasz Kasprzak2026-08-191-11/+30
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | test_lang_coverage.ml's own coverage test used to walk only the OBSERVED day of each date (one Celebration.t per Liturgical_day.t). A liturgical day also carries a second stream of slugs -- commemorations (kept when the observed day does not fully displace a losing candidate, RG 108-111) and transfers (an impeded I/II-class feast moved to a later date, RG 96-98) -- and nothing here ever looked at them. The test asserted coverage of what it happened to WALK, not of what the engine can EMIT, so it passed green while the ordo booklet printed raw slugs ("Commemoratio canute-martyr", "Commemoratio maur-abbot", "Commemoratio peter"). test_lang_coverage.ml now walks observed, every entry in commemorations, transferred_in, and every entry in transferred_out. That extension turned up 120 slugs with no Latin name across 2020-2045, now added to lang/la.ini: 112 are data/ef/sanctoral.sexp companions the temporal-only walk never reached; 5 (barbara, commemoration-of-st-peter, commemoration-of-the-seven-sorrows, major-litanies, rogation-wednesday) are data/ef/adjustments.sexp's own hand-authored Add directives; 3 (ef-nativity-octave-day-2/3/4) are temporal days with no calendarium heading to transcribe, built by the same pattern days 5-7 already use. Seven of the sanctoral names are cited to docs/research/scan1.txt rather than LT.txt: the 2006 web-capture transcription silently drops several real commemorations that both photographic scans carry (donatus, romanus, eusebius-confessor, naboris-et-felicis, sts-gervasius-and-protasius, sts-felix-and-adauctus, and praxedis-virginis -- the last for a spurious ligature LT.txt introduces, "Praxedis" confirmed correct against both scans). The same 120 names, extracted verbatim from sanctoral.sexp's and adjustments.sexp's own English fields rather than retyped, are added to lang/en.ini. make check-citations: 400 LT.txt citations checked, 0 wrong, 0 malformed, 0 cannot verify. Teeth proved by deleting canute-martyr's own name (a Commemoration_only slug that can never be the observed day, only ever a commemoration) and confirming the coverage test fails naming exactly that slug, then restoring it.
* feat(templates): the wall calendar fills its pageLukasz Kasprzak2026-08-193-608/+656
| | | | | | | | | | | | | | | | | | | | | | | | | | | | The previous version occupied about the top quarter of an A4 page and left the rest blank, and printed slugs. Cell height is now computed from the text height (\textheight-18mm over 6 rows -- every month has 5 or 6 Sunday-started weeks) rather than left to the table's natural size, which is what made it float at the top. Measured: January 2027's last row of content now reaches y=566pt of a 595pt-tall page, not a quarter of the way down. Real names replace the raw slug in every cell (the observed day's own resolved display name, a plain string per View.of_days), and each day's rank name is shown too, so a printed page reads as a calendar rather than machine keys. Weekday headings come from the view as a localised list rather than a hard-coded Dom/Lun/Mar row, so a translated calendar needs no template edit -- and because the engine rejects an empty tag path, the list carries named {name; last} fields rather than bare values. grid.ms and grid.html carry the same two changes (real names, localised weekday headings) for their own flavours. Verified with pdflatex/groff: 12 pages, A4 landscape, every row exactly 7 cells (6 ampersands), zero warnings. Goldens regenerated through the test harness's own render path, not the CLI, whose default language table differs from the test's.
* fix(templates): reserve room for the ordo booklet's running headerLukasz Kasprzak2026-08-191-1/+14
| | | | | | | | | | | | | | | | | | | | | | geometry's top=11mm/bottom=12mm seated the text block without accounting for fancyhdr's own header/footer, which live OUTSIDE that block by default -- pushing the running header ('Ordo 2027 . ef') partly above the physical page edge (measured: yMin -5.5pt on a 0..595pt page) and the footer to within 1pt of the bottom edge. The user hit this and reported the header as clipped. Adds includehead/includefoot to the geometry options (a5paper, top=10mm, bottom=10mm) so the header/footer live inside the margins instead, with headsep/footskip set explicitly (3mm/7mm) rather than left at article's defaults -- those defaults alone would still overflow a 10mm margin and silently added 20 extra pages to the whole booklet by shrinking every day box. Verified: two pdflatex passes produce a 65-page A5 PDF with the full 'Ordo 2027 . ef' header intact on every page and zero pdflatex warnings. Golden regenerated through the test harness's own render path (English-with-Latin-fallback), not the CLI, which defaults to Latin only and would otherwise pin output the tests never produce.
* feat(templates): the ordo booklet, rebuiltLukasz Kasprzak2026-08-196-6664/+8248
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A5, one week per page, with a table of contents, each day in a framed box and a colour swatch so the page is scannable without reading. Real names throughout, and every fixed string routed through the term vocabulary so a translated booklet needs no template edit. The previous version was A4, printed slugs, ran days together with no visual separation, had no contents, and gave the liturgical colour only as the word white. It was not usable as a printed booklet. lib/render/view.ml already carried month_num/month_name on each week object from Task 5, so no kernel/view change was needed here -- the template uses those instead of a parent path the engine cannot express. Box padding, margins and secondary-line font size were tightened past the brief's own starting values: at the brief's sizes a week with several long commemoration names (e.g. Feb 21-27, three of seven days carrying one) overflowed onto a second physical page, breaking one-week-per-page. Verified by measuring page count against the known week count (63 weeks in 2027) until every content page carried exactly one week, not by eye. The header comments of all six flavours were themselves a trap the brief warns about: writing double braces to NAME a template field inside a LaTeX %, groff .\", HTML <!-- -->, or AsciiDoc // comment gets parsed as a real tag by this brace-only engine, not treated as inert prose. An early draft's own comments did this and produced 'empty tag path' parse errors and a silently-unclosed section; every comment in all six templates is now written without ever typing two braces in a row. The other five flavours keep their existing structure; only the dead {{#name}}{{la}}{{^la}}{{slug}}{{/la}}{{/name}} idiom is replaced with a plain {{name}} (extended to comms entries too, which carry their own resolved name), and the fixed labels (Ordo, Epistle, Gospel, Commemoration) now come from {{term.*}}. Golden regeneration could not follow the brief's own `colitur table` shell-out literally: the CLI's current default language is Latin-only (bin/main.ml's Task-5 bridge), while test_render_golden.ml renders through Test_view's English-primary/Latin-fallback table, so the two produce different text for the same slug. The goldens were regenerated through the test's own render path instead (a temporary env-gated block in test_render_golden.ml, reverted before this commit), so they agree with what the suite actually computes. All six ordo golden tests pass; the three grid ones are Task 9's scope. make check-templates passes with zero warnings. The CLI-rendered PDF is A5 (148x210mm), 65 pages -- 2 pages of title/contents plus exactly one page per week (63), verified with no overflow.
* feat(render): names reach the view and every emitterLukasz Kasprzak2026-08-193-11/+110
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* feat(lang): Latin sanctoral names, and EnglishLukasz Kasprzak2026-08-192-20/+52
| | | | | | | | | | | | | | | Sanctoral names are transcribed from the Missal's own calendarium and kept in the GENITIVE, as the Missal prints them -- noted in the file so nobody corrects them to the nominative. English reuses the 327 names already in data/ef/sanctoral.sexp rather than retyping them, and declares fallback = la, so an untranslated day in an English booklet shows Latin rather than a slug. The test asserts the FALLBACK works rather than that en.ini is exhaustive: that is what makes a partial translation shippable from its first line. Coverage now demands a name for every slug the engine can emit across 2020-2045, temporal and sanctoral alike.
* feat(lang): Latin temporal names from the MissalLukasz Kasprzak2026-08-193-0/+75
| | | | | | | | | | | | | | | Every entry is transcribed from the 1962 Missal's own propers headings in docs/research/LT.txt and cites where it came from; names constructed by following a neighbouring pattern are marked as such, so a reader can tell transcription from inference. The coverage test is the point of this commit. It walks every day of 2020-2045 and fails naming any slug with no Latin name -- the test that would have caught the original defect, where a printed booklet said ef-septuagesima-sunday-2 because nothing asserted that names exist. The three Triduum names reuse the exact strings temporal_ef.ml already carries, so the engine and the language file cannot disagree.
* fix(naming): config.ml merges every [defaults] block, like lang.mlLukasz Kasprzak2026-08-191-1/+45
| | | | | | | | | | | | | | | | | | | | | config.ml and lang.ml both parse the INI format through the same reader, Colitur_kernel.Overlay_ini.parse_sections, but resolved a repeated [section] header oppositely: lang.ml folds over every section sharing a name, while config.ml used List.find_opt and silently discarded every [defaults] block after the first. Two modules parsing one file format must not disagree about what a duplicate section header means. of_string now folds a single accumulator across every section named [defaults], in file order, matching lang.ml's of_string shape. A scalar key (lang/template/format) repeated across two blocks resolves to the later value, consistent with the existing within-section last-wins rule; overlay keeps accumulating across every block, not only the first; and unknown_sections still excludes every [defaults] block, merged or not, since merging it is the point. config.mli's lang doc comment is extended to say the last-wins rule holds across block boundaries too, cross-referencing lang.ml's own duplicate-section policy so the two do not drift again unnoticed.
* fix(naming): config fix round 1 -- unknown sections, O(n) accumulateLukasz Kasprzak2026-08-191-3/+19
| | | | | | | | | | | | | | | | | | | | | F1: test_unknown_key_is_reported_not_fatal never asserted unknown_keys itself, only that parsing survives -- a no-op accumulator passed it. Now asserts the key is actually collected. F2: a misspelled section name, e.g. [deafults], was silently discarded -- Ok empty, lang and everything else gone, nothing reported. That is the highest-value typo this feature exists to catch. Any section other than [defaults] is now collected into a new Config.unknown_sections, kept separate from unknown_keys so the CLI can word the two warnings differently. Still non-fatal: a newer colitur's added section must not break an older binary. F3: overlays and unknown_keys accumulated with '@ [v]' per line, O(n^2) over the field count. Cons during the fold, List.rev once at the end. F4: documented that lang/template/format are last-wins on a repeated key, the opposite direction from Overlay_ini.get's first-wins over the same section type.
* fix(naming): merge duplicate [section] blocks in the language tableLukasz Kasprzak2026-08-191-1/+33
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | F1 (review round 1): of_string's find took only the FIRST section of a given name (List.find_opt), so a second [celebration] block anywhere in the file was silently dropped in its entirety -- reproduced with two blocks (a in the first, b in the second): b resolved to the slug fallback "b", not its real value. This is a data-loss footgun aimed squarely at what happens next: Tasks 3/4 write a 595-entry, hand-edited la.ini, and appending a second [celebration] block is the natural way to paste in a new batch of names. Worse, the failure surfaces nowhere near its cause -- a coverage check reports the dropped slugs as missing a Latin name, with nothing pointing at the parser. find now folds over every section sharing the name, in file order, so all blocks merge. This also settles which value wins when the same key appears in two different blocks: later in the file wins, consistent with the existing within-one-block behaviour (unchanged, still last SM.add wins) and with what a reader expects when appending to an INI file. lang.mli now documents both duplicate policies explicitly, and notes they run OPPOSITE to Overlay_ini.get's first-match (List.assoc_opt) over the same section.fields shape -- undocumented before, and a latent trap since the two modules read the same section type but resolve a duplicate key in opposite directions. Three tests added: two [celebration] blocks both resolve (the F1 regression), a key repeated across two blocks resolves to the later block, and a key repeated within one block still resolves to the later line (confirms unchanged behaviour). Confirmed the regression test fails against the pre-fix code (b resolves to "b", the slug fallback) and passes after.