aboutsummaryrefslogtreecommitdiff
path: root/test/cli.t
Commit message (Collapse)AuthorAgeFilesLines
* docs(cli): --help's rubrics section drifted to three fields, it has fourLukasz Kasprzak2026-08-221-0/+17
| | | | | | | | | | | | | | | | | | | | Task 5 (celebrant-rubrics-phase1) added a fourth rubrics column, creed, and updated the man page for it. bin/main.ml's own built-in --help was not: its "output formats" section and worked example still showed three fields (date, formulary slug, source), and nothing caught it because test/cli.t pins command dispatch but never the help body. Fixed the format line and its example, and added a short mention of the creed column to the surrounding prose paragraph (source, format, what --overlay/--lang/--raw do to it), matching the man page's own account without copying it verbatim. test/cli.t now cross-checks --help's own rubrics example against what `colitur rubrics 2026` actually prints for the identical day, so the two cannot silently drift apart again in either direction. Mutation-proved: reverting the field count in --help alone (output unchanged) reddens exactly this new check; reverted clean.
* fix(kernel): Mass_formulary.t.said is honestly optional -- was false for VotiveLukasz Kasprzak2026-08-221-12/+24
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The .mli promised said is "the slug whose Mass is said". For Votive (RG 78/309(a), the Saturday votive Mass of Our Lady) it was set to the day's own ferial slug -- whose Mass is exactly the one NOT said. A consumer joining rubrics to readings on that slug would silently get the wrong Mass: 2026-01-03 reports ef-christmas-1-saturday, which has zero entries in data/ef/lectionary.sexp, because the citations actually come from bvm_saturday_citations, a season-keyed function with no slug of its own anywhere in the shipped data. Chose the type-honest fix over the interim documentation one: said is now Slug.t option, None exactly for Votive, because there is genuinely no slug in the shipped data this field could report for that one source. Adding real ids for the five seasonal BVM Masses (the reviewer's first option) is out of scope -- a data restructuring this round explicitly does not carry. Threading the office slug through a second field was considered and rejected as redundant: the day's own office is already available on the same Liturgical_day.t via observed.slug, which every caller already has in scope regardless of via, so said does not need to duplicate it. colitur rubrics stays byte-identical: rubrics_line already has d.observed in scope and falls back to its slug when said is None, printing the exact value it always printed for a Votive row (verified directly, diffed against pre-fix output across four years). colitur day/readings are unaffected (neither reads Mass_formulary at all). colitur emit --format sexp's pretty-printed line count for 2027 moved 9011 -> 9025: every day's formulary record widened by said's own extra option wrapping, and to_string_hum wraps by column width. Cosmetic only, diffed line by line to confirm every change is this shape or a consequent wrap shift; recorded in test/cli.t alongside the 476(f) note it now sits next to.
* fix(ef): RG 476(f) -- the Creed is not said at a Requiem MassLukasz Kasprzak2026-08-221-2/+12
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | colitur said the Creed IS said on All Souls' Day and every other Requiem-shaped day: RG 475(b)'s "in festis I classis" branch fired on rank alone, with nothing checking RG 476(f), "Non dicitur symbolum ... f) in Missis defunctorum". Allow-listed as L1 in expected-divergences-lms.sexp on the argument that [creed] has no notion of a Requiem Mass in its inputs and could not express the rule without a signature change. That argument does not hold. Colour.Black is used by exactly two celebrations in the whole engine, verified by grepping every occurrence in lib/ and data/: commemoration-of-all-souls (data/ef/sanctoral.sexp) and Good Friday (temporal_ef.ml, already false via the earlier RG 23(b) feria exclusion and has no Mass at all in the 1955-restored Holy Week). So on every day this engine can construct, colour = Black if and only if the Mass is a Requiem -- a citable implication (RG 117 assigns black to Masses of the dead) on this data, not a heuristic. Rite_ef.Rubrics_ef.creed now reads observed.colour as RG 476(f)'s own guard, positioned ahead of 475(b)'s rank branch. A new test, test_colour_black_population_is_exactly_two, sweeps both the shipped data and Temporal_ef's own code across a century and fails loudly the day a third Colour.Black celebration is ever introduced, so the proxy cannot silently rot. L1 is closed in expected-divergences-lms.sexp, citation preserved. Blast radius, measured (full 1583-9999 sweep, grouped by observed slug): 8 416 days change from true to false, every one commemoration-of-all-souls (missing from exactly 1 of the 8 417 domain years, a boundary-year transfer effect at 1583/9999). A further 8 416 Colour.Black days are Good Friday, unaffected (already false before this fix). colitur day/readings stay byte-identical to v0.10.1 (checked directly against a v0.10.1 worktree build, 9 domain-spanning years); colitur rubrics changes on exactly the 8 416 days above. colitur emit --format sexp's pretty-printed line count for 2027 moved 9010 -> 9011 as a side effect: Sexplib.Sexp.to_string_hum wraps by column width, and All Souls' Day 2027's record grew one wrapped line because "false" is one character longer than "true". Cosmetic only -- no field's data changed -- and pinned in test/cli.t with the measurement recorded.
* feat(ef): the Creed, RG 475-476Lukasz Kasprzak2026-08-221-9/+9
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Whether the Creed is said at Mass. New Rubrics_ef.creed, reached through a new Rite.t.creed field and a new Liturgical_day.t.creed bool (false, not an option, for a rite that has not implemented the rule); colitur rubrics gains a fourth TAB-separated column. 475(a) reads the TEMPORAL cycle's own weekday, not the observed day: a Sunday impeded by a Feast of the Lord (RG 16a) still says the Creed. 475(d)'s three octaves (Nativity, Easter, Pentecost) are pure date/ Easter-offset windows, checked first, since a saint's feast winning the day inside one of them still says the Creed ("etiam in festis occurrentibus") -- St Stephen, 26 December, is the live witness. RG 23 classifies Ash Wednesday and every feria of Holy Week (including the Sacred Triduum) as feriae, not festa, however high their rank, so 475(b)/(c)'s "in festis" never reaches them; this single check subsumes 476(a)'s own naming of the Chrism/Lord's-Supper Mass and the Easter Vigil. RG 28-34 vigils are a third liturgical-day category, also excluded from "in festis" regardless of rank -- reuses the already- exported Precedence_ef.is_vigil rather than a new list. 475(e)'s Apostle/Evangelist natalicia list (creed_apostle_slugs) was derived by grepping data/ef/sanctoral.sexp directly and checking each candidate's own date against whether it is that saint's dies natalis -- not copied from any list supplied with the task. The Conversion of St Paul and the 30 June Commemoration of St Paul are excluded (neither is a natalicium); the Chair of St Peter and St Barnabas are included only because the clause names them explicitly, which is exactly why it has to: neither is a natalicium either. 475(c)'s BVM half reuses Precedence_ef.marian_slugs (newly exported) rather than the subject field alone: checked against the shipped data, almost every Marian sanctoral entry ships subject=Saint, not Bvm. man/colitur.1's rubrics section is updated to match the new column; test/cli.t repinned via dune promote for the same reason. Verified day/readings output byte-identical to v0.10.1 across the whole 1583-9999 domain (both binaries' concatenated day+readings output, 6,148,492 lines each, zero diff). Domain-wide: 882,996 days say the Creed, 2,191,250 do not; every one of the domain's 439,178 Sundays says it, zero exceptions.
* 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.
* feat(kernel,ef): the lectionary reports which Mass it saidLukasz Kasprzak2026-08-211-1/+1
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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(config): flavour is settable, and an INI overlay is diagnosedLukasz Kasprzak2026-08-211-0/+19
| | | | | | | | | | | 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: audit findings — parser strictness, name ambiguity, and errorsLukasz Kasprzak2026-08-201-5/+5
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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-201-8/+15
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* feat(lang): Latin and English book names, and the shipped sigla stylesLukasz Kasprzak2026-08-201-16/+21
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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-201-5/+87
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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-201-12/+87
| | | | | | | | | 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(naming): the rite prints its display name, not the internal id efLukasz Kasprzak2026-08-201-2/+2
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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(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.
* feat(render): names reach the view and every emitterLukasz Kasprzak2026-08-191-7/+16
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The view's name is now the RESOLVED display string and slug is untouched, so machine formats carry both -- a script keeps the stable key, a human reads the name. name is a plain string, not a lang-keyed object. That removes the shadowing hazard outright: a dotted {{name.la}} used to fall back WHOLESALE to the enclosing month's name.la and print Ianuarius on every unnamed day, which is how a printed booklet came to show the month where the feast belonged. weekday, season, rank and colour all gain localised companions, because a calendar in a language needs more than feast names, and templates gain a term vocabulary so fixed strings need no template edit to translate. Asserted over a whole year: no day renders its slug as its name. Beyond the brief's own code sample: - comm_value's own name is now resolved through the same lang.celebration table too (not only the observed day's), because Task 8's own ordo template interpolates a plain {{name}} inside {{#comms}} -- an Obj there would render silently blank. A commemoration slug without Latin coverage still degrades to the slug, same as everywhere else in this system; that is a lang/la.ini DATA gap (113 of 327 sanctoral slugs, measured), not a regression this task introduced. - bin/main.ml's emit/table/publish call sites needed ~lang to compile at all, which is collateral from the of_days signature change, not this task's own file list. Rather than pass Lang.raw and ship the very slug-as-name defect this branch exists to fix, they load the shipped Latin table by the same probe order data_dir() already uses -- a deliberate, commented BRIDGE that Task 6 replaces wholesale with real --lang/--raw/config resolution. bin/dune gained colitur_naming accordingly. - test/cli.t needed two related fixes to stay green: the CSV header/row example, and a table/LaTeX escaping demonstration that relied on the kernel's own English name for Sts Peter & Paul -- gone from the view now that name resolves through lang tables only, and the Missal's own Latin spells the feast with et, never an ampersand. Escaping itself is still proved live on 2035 data in test_emit.ml. - Both schemas gained the new day/week/top-level keys (season_name, weekday, rank_name, colour_name, term, weekday_headings, month_num, month_name), not only the name shape change; schema/colitur-v1.xsd verified against real emitted XML via xmllint (make check-schema). Render/golden's 9 cases (the shipped ordo/grid templates, all six flavours) now fail as expected: their old {{name.la}} / {{#name}}... idiom finds nothing on a plain string. That is Tasks 8/9's own scope to rewrite, per the plan's own pre-flight conflict scan -- not fixed here, and not silently pinned by regenerating goldens off broken output. 495 tests run (490 + 5 new), 486 pass; the 9 failures are exactly Render/golden's ordo/grid cases.
* fix(cli): guard publish's IO, validate --dtstamp, and list all commandsLukasz Kasprzak2026-08-191-3/+40
| | | | | | | | | | | | | | | | | | | | | | | | | | | publish's own mkdir_p/write_file (unlike every other IO path on this branch) were unguarded: an unwritable --out parent raised a bare Unix.Unix_error(EACCES,...) and an --out naming an existing file raised ENOTDIR, both as uncaught exceptions with a stack trace rather than the project's one-line "colitur: ..." form. The same defect class commit 6bd741b already fixed once for template reads -- --out is user input too. Fixed by wrapping the whole publish_report call (not each write_file site) in one handler for Unix.Unix_error and Sys_error, mirroring why that earlier fix guarded the whole read and not only the open. Added a cram case using a read-only directory inside the test's own cram sandbox, not /tmp, so a failed cleanup cannot leave an unwritable directory behind in a shared location. --dtstamp was the only user string reaching output unescaped and unvalidated: "--dtstamp hello" silently emitted an invalid "DTSTAMP:hello", and a value carrying its own CRLF injected extra lines into every VEVENT. Fixed by rejecting anything not matching RFC 5545's UTC form (8 digits, 'T', 6 digits, 'Z') before either emit or publish does anything else, one line to stderr, exit 2. usage() was byte-unchanged from before the branch and listed only the six pre-existing commands, omitting all four commands this branch added (emit, table, render, publish). Added them; the three cram pins of the exact usage string are updated to match.
* fix(cli): publish --prune refuses a manifest entry that escapes --outLukasz Kasprzak2026-08-191-0/+48
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | CRITICAL: .colitur-manifest lives INSIDE the tree publish writes into -- the very tree this feature exists to have committed into a git repo. A manifest entry with a ".." path component, or an absolute path, let --prune Sys.remove/Unix.rmdir a file OUTSIDE --out. No attacker is required: an ordinary bad merge, a conflict resolved the wrong way, or a hand-edit of that file is enough to plant such an entry, and publish's own stated contract -- it never deletes a file it does not own -- broke outright the moment one was present. Two independent checks, both required, applied before every deletion: - structural (manifest_entry_is_safe): reject an entry that is absolute or has a ".." path COMPONENT, by splitting on '/' and comparing components, not by substring-matching ".." (which would wrongly reject a legitimate name like foo..bar). - containment (resolves_under): resolve both --out and the candidate with Unix.realpath (closing a symlink-inside-out gap the structural check alone would miss) and verify the candidate is a genuine path descendant of --out, not merely a string with the same prefix. Applied at both the file-deletion loop and prune_empty_dirs' own directory removals. A rejected entry is skipped with a one-line stderr warning; publish completes rather than aborting -- a corrupted manifest must not make the tool itself unusable. test/cli.t reproduces the exact canary scenario (a ".." entry surviving deletion of a file outside --out), an absolute-path entry, and a legitimate dotted filename (no .. component) still pruning normally, alongside the existing --prune coverage.
* feat(cli): colitur publish -- the static treeLukasz Kasprzak2026-08-191-0/+442
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Writes ef/<year>.{json,csv,xml,ics}, one JSON per day, the schema and a generated index. That tree is the API: any web server or git repo serves it, and nothing runs at request time. Deterministic: publishing twice is byte-identical, asserted in cli.t. That is what makes publishing into a git repo safe -- the diff shows only real change, and you review it before pushing. Non-destructive: a manifest records exactly the files this tool wrote, so --prune can only remove files a previous run created. A file you put in the output directory yourself is never touched, with or without --prune. Asserted in both directions. Pruning a stale file also removes any directory it leaves empty behind it (e.g. an old year's own ef/<year>/ tree), stopping at --out itself -- without this, a pruned year's own directory would survive empty and test -d would still see it. schema/day-v1.json is resolved the same prefix-relative way data/ef's own sexp files are (installed vs build-tree, probed rather than assumed), never from cwd, and a missing schema fails with one line on stderr before anything is written rather than emitting an empty file. Needed schema/day-v1.json wired into the root dune file's default alias and into test/dune's cram deps -- unlike data/ and templates/, nothing made dune mirror schema/ into the build tree before this. unix is added to bin/dune's libraries for mkdir_p; it ships with the compiler, so colitur.opam and dune-project are unchanged.
* fix(cli): guard the whole template read, not only the openLukasz Kasprzak2026-08-191-0/+9
| | | | | | | | | | | | | | | | | | read_file guarded open_in_bin but left in_channel_length and really_input_string unguarded, so a path that opens but cannot be read as bytes -- a directory -- escaped as an uncaught Sys_error and crashed the program, leaking the open channel on every failure path. A template is user input; it must never crash the program. Wrap the whole read in Fun.protect so the channel closes on every path (success, exception, early return), matching the close-on-every-path pattern already used in the test suite. The missing-file message stays exactly as before; a read failure after a successful open now carries the exception text, the same path: exception shape Layer.load and Overlay.load already use. New cram case points --template at a directory (the sandbox's own cwd, not /tmp) and asserts one stderr line and exit 2, not a crash.
* feat(cli): colitur table and renderLukasz Kasprzak2026-08-191-0/+69
| | | | | | | | | | | | | | | Computes and renders in one process. There is deliberately no stdin-fed render: honouring the pipe would need a JSON parser we would have to write, purely to serialise and immediately re-parse our own view -- a second hand-rolled component and a second place for the contract to drift, for no benefit. colitur emit --format json | jq still composes. An unknown extension with no --flavour is an error naming the six valid flavours, never a silent fallback to none: guessing wrong produces malformed output that looks fine until it does not. A malformed template reports the parser's own reason and exits 2. A template is user input; it must never crash the program.
* feat(cli): colitur emit -- csv, json, sexp, xml, icsLukasz Kasprzak2026-08-191-0/+69
| | | | | | | | | | | | | | | 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(examples): ship two real local calendars, with their limits statedLukasz Kasprzak2026-08-181-0/+23
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* feat(overlay): a flat INI front end, which verifies its own outputLukasz Kasprzak2026-08-181-0/+19
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* feat(cli): colitur check and colitur new-overlayLukasz Kasprzak2026-08-181-3/+35
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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(data): ship a worked example overlay calendarLukasz Kasprzak2026-08-181-0/+5
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | `--overlay` shipped last week with nothing to point at. The only example was a test fixture, explicitly labelled invented and living where no user would find it, so "colitur supports local calendars" was true but undemonstrated. data/ef/examples/diocesan-example.sexp is installed to <prefix>/share/colitur/examples/ and is runnable documentation: every celebration in it is invented, with example- slugs that cannot collide, and its header explains the four directives, the three date shapes, and the field vocabularies before showing any of them. It demonstrates the range deliberately rather than the minimum: a fixed-date local patron with its own propers; a movable dedication on the first Sunday of October, I class because a church's own dedication anniversary is I class in that church and at III it would lose to the Sunday every year; an Easter-relative commemoration; an Edit raising a universal feast for local use; and a Suppress shown commented out, because suppressing a universal feast is a real liturgical act and an example should not invite it casually. Two things the header says plainly, since this is the one file that lets someone change what colitur computes: an overlay is applied ON TOP of the shipped calendar and never instead of it, and an overlay is applied, NOT validated -- the five test layers assert things about the shipped data and cannot vouch for a user's file. A test asserts it loads against the real EF rank vocabulary, still demonstrates at least four directives, and applies with NO diagnostics. The last matters most: a directive naming a slug the universal calendar lacks would warn, and an example that warns teaches the wrong lesson. It deliberately does not pin which days it produces -- those celebrations are invented, and pinning their dates would make an illustrative file behave like calendar data.
* test(cli): the 3 January pin is a BVM Saturday, not a feriav0.3.0Lukasz Kasprzak2026-08-171-1/+8
| | | | | | | | | 3 January 2026 is an unoccupied IV-class Saturday, so its office is Our Lady's and now its Mass is too -- the second of the Missal's five, assigned to Christmas through the Purification. The pin held the Christmastide feria's readings, which is what colitur said before RG 309(a) was implemented. Caught by make check on the 0.3.0 release, which is what that gate is for.
* test(cli): pin the version's shape, not its literalv0.2.0Lukasz Kasprzak2026-08-171-4/+9
| | | | | | | | | | | The release target bumps bin/main.ml and dune-project, but cli.t pinned the literal too, so cutting 0.2.0 failed `make check` on a cram diff that was purely the bump working correctly. Every release would have had to edit a test for no gain. The shape is asserted here instead. The value is already checked where it can actually drift: the release target requires the freshly built binary to report the version it just wrote.
* feat(cli): --overlay FILE, so a local calendar can be suppliedLukasz Kasprzak2026-08-171-0/+65
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The Overlay algebra has been built and tested since Plan 2, and Date_spec grew movable variants last commit, but there was still no way to hand colitur a file: bin/main.ml applied exactly one overlay, the shipped data/ef/adjustments.sexp, with the path hardcoded. This is the plumbing. --overlay is repeatable and ordered, and applies ON TOP of the shipped adjustments rather than instead of them. That ordering is the whole point: adjustments.sexp carries RG 110's own 30 June companion, the Major Litanies, St Barbara and Rogation Wednesday, and a user file that replaced it would silently drop all four while appearing merely to add a local feast. Overlay.merge is last-writer-wins, so a local calendar can still override a universal entry deliberately, by naming its slug. The dispatch needed real argument parsing. It matched Sys.argv as an exact array, which does not survive a repeatable flag -- two --overlay arguments are a different array shape from one, and each further flag would multiply the patterns again. Flags are now stripped first and the remaining words matched as command plus year. Hand-rolled, because the dependency list is frozen and this is fifteen lines. One defect found by the cram suite on the first run: the unknown-option guard rejected --help and --version, having excluded only their short forms. Refused, not ignored, on easter and temporal. Neither reads sanctoral data -- temporal deliberately runs the cycle before any layer exists -- so accepting the flag there and silently doing nothing is the failure mode this project refuses everywhere else. A file that fails to load is fatal, exactly as the shipped overlay is; a directive naming a slug that does not exist warns on stderr and the run continues. The second matters more for a user file than for ours: a typo in a diocesan calendar should say so rather than quietly do nothing. test/fixtures/overlay-example-diocesan.sexp is a worked example and is labelled as invented, not calendar data. It carries both shapes a real local calendar needs: a fixed-date patron, and a dedication on "the first Sunday of October" -- which is the case the Date_spec work existed to unlock and which lands on 4 October in 2026, 3 October in 2027, 1 October in 2028. The dedication is I class because a church's own dedication anniversary is I class in that church; at III class it lost to the II-class Sunday every year, which a first draft demonstrated correctly and uselessly. The man page gains an OVERLAYS section saying plainly that an overlay is applied, NOT validated: the five test layers assert things about the shipped calendar and none of them can vouch for a file the user supplies. This is the first feature that lets someone change what colitur computes, and that distinction should not have to be inferred.
* feat(cli): --version, a CHANGELOG, and a release targetLukasz Kasprzak2026-08-171-0/+18
| | | | | | | | | | | | | | | | | | | | | | | | Groundwork for tagging. The project had no version anywhere: not in dune-project, not in the binary, no CHANGELOG, no tags. The version lives in two places -- dune-project, which generates colitur.opam, and a constant in bin/main.ml, which is what --version prints. Two rather than one because dune's watermarking only substitutes in a release tarball, so a binary built the ordinary way from a checkout would report a placeholder. The release target rewrites both and then re-checks both, and finally requires the freshly built binary to report the version it just wrote: a release that bumped one and not the other would ship a binary disagreeing with its own package metadata. --version is deliberately not embedded in the help text. cli.t pins help's first line, and a version there would mean editing a test expectation every release for no gain. The release target mirrors lectio's, refusals included: no release from a dirty tree, none without a CHANGELOG entry for that version, none whose version bump silently failed to apply, and `make check` -- the full 1583-9999 sweep, not the sampling suite -- must pass before the tag is created.
* feat(cli): a Makefile, a man page, and --helpLukasz Kasprzak2026-08-171-1/+31
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Three things the project had no answer for: how to install it without knowing dune, where to read about it, and what it does when asked. Makefile, same shape as lectio's -- PREFIX ?= $(HOME)/.local, BINDIR, MANDIR, and the '## '-comment help target -- so the two siblings are driven the same way. Every recipe wraps dune in `opam exec --`, which is the actual point of having one here: `make build` works from a plain shell with no `eval $(opam env)` first. install goes through `dune install` rather than a hand-rolled copy, because the binary finds its calendar data relative to its own path; the man page is installed separately to share/man/man1, matching lectio. install and uninstall were both run against a scratch prefix and checked: uninstall leaves zero files behind. PREFIX defaults to ~/.local because that is where lectio installs and where it actually lives on this machine, so colitur lands on an existing PATH with no shell change. An earlier install this session went to ~/opt/colitur, which was me over-applying a rule meant for third-party tools to one of the author's own projects; it has been removed rather than left as a second, staler binary competing on PATH. man/colitur.1 documents the four commands, both output formats and why they differ, COLITUR_DATA_DIR and its refusal to fall back, the data resolution order, exit statuses, and -- deliberately -- the limitations: EF only, Epistle and Gospel only with the chants unbuilt and rejected rather than guessed, and the BVM Saturday Mass-selection gap. A man page that only lists what works is half a man page. Renders clean under `groff -ww -z`, no warnings. --help prints to stdout and exits 0; a usage error prints one line to stderr and exits 2. That is the Unix convention rather than a preference: asking for help succeeded and should be pipeable, being invoked wrongly did not and must not pollute stdout. Both directions are asserted in cli.t, along with a loop confirming every command the help text advertises is one the dispatch actually accepts -- the check that catches help drifting away from the code.
* feat(cli): install the runtime data, and resolve it in three waysLukasz Kasprzak2026-08-171-0/+20
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | `dune install` produced a colitur that could not run: the binary locates its data relative to its own path, nothing installed the four .sexp files anywhere, and an installed colitur exited 2 unable to read sanctoral.sexp. `dune build @install` produced an empty tree. data/dune installs the four RUNTIME files into <prefix>/share/colitur/ef. Deliberately not the two allow-lists: those describe where colitur and a comparison oracle disagree, are read only by the differential and oracle tests, and are meaningless to a running colitur -- installing them would ship an assertion about lectio's and missalemeum's behaviour as though it were calendar data. No glob, on purpose: a glob would silently start shipping them again the moment one was renamed. Resolution now probes rather than computing one path and hoping. An installed prefix is tried first, the build tree second, and a candidate counts only if sanctoral.sexp is actually readable inside it -- so a failed or half-removed install falls through to a working tree instead of shadowing it and failing later with a per-file error. COLITUR_DATA_DIR overrides both and NEVER falls through. This was the one real design question and the first version got it wrong: it treated the override as just another candidate, and a deliberately bogus value produced a full, plausible, entirely un-flagged year computed off the build tree. Someone who names a directory has stated an intent, and silently calendaring off a different one is the silent substitution this project refuses everywhere else. It is now exit 2 naming the directory. Verified end to end, not inferred: installed to a scratch prefix, then ran the binary from an unrelated cwd with no build tree near it, and confirmed 2038-03-06 still resolves Perpetua and Felicitas through the Common route. All four paths exercised -- build tree, valid override, bogus override, and an empty share/ falling through. Environment reads stay confined to bin/. The kernel's contract forbids them and nothing below the CLI learns where the data came from; the loaders take a path.
* feat(cli): colitur readings, one line per dayLukasz Kasprzak2026-08-171-1/+73
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The lectionary has been resolvable since Task 4 but invisible from the command line: `colitur day` prints no citations, so the branch's whole deliverable could only be seen through the test suite. A separate command rather than extra columns on `day`, which is where the plan pointed. Its instruction was to append the Epistle and Gospel to each row "matching the existing column style", and that turns out not to be possible: a citation contains spaces and commas ("Ezech 34:11-16", "Ecclus 51:1-8, 12") while a day row is space-separated with a variable-length +slug commemoration tail, so appending them leaves the row unsplittable -- no awk or cut field number recovers where the Epistle ends. That is the opposite of what the row is shaped for. So `day` keeps its format byte-identical, asserted directly in cli.t rather than left implicit, and the citations get their own row with " | "-delimited fields, safe for values containing spaces. Both formats are a stopgap and say so in the source: the design calls for one schema rendered through a logic-less template engine, and two ad-hoc formats are easier to retire later than one overloaded format whose parsing rules nobody wrote down. The year walk is now shared. day_report and readings_report differ only in how a day is printed, and the two-liturgical-year indexing -- with its own reasoning about civil-versus-liturgical spans -- is exactly the part that must not be duplicated and drift. "-" for an absent part, though no EF day can currently print one: layer 2 asserts exactly one First and one Gospel on every day of every year 1583-9999. The CLI still does not assume a guarantee the kernel makes about data rather than about types. cli.t gains the four chain-step cases test_golden.ml pins against the scans, so the CLI path is checked to agree with the library path, plus the 2 January line, which reads the Circumcision's Mass under the Missal's own ferial rubric (scan1:6523-6526) and not RG 17(a). Verified the cram tests actually run and have teeth: corrupting one expectation produces a diff and exits 1. CLAUDE.md: state, test count (369, 370 with the sweep), the readings command and why it is separate, and What's next -- the lectionary landed early rather than inside Plan 4, so what remains there is OF's own lectionary, not the mechanism. Chants stay deliberately unbuilt, and Validate now rejects any part outside First/Gospel.
* rite(ef): the Annunciation's RG96 exception is conditional, not unconditionalLukasz Kasprzak2026-08-121-0/+13
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The register was transcribed faithfully but was itself wrong: RG 96's Attamen (a), primary-source-verified from the scans and now corrected in the register, reads 'festum Annuntiationis B. Mariae Virg., quando est transferendum post Pascha, transfertur ... in feriam II post dominicam in albis' -- the Monday-after-Low-Sunday seat applies ONLY 'quando est transferendum post Pascha', when the feast is to be transferred PAST EASTER. The unconditional transcription made the exception fire on every impeded Annunciation regardless of cause. transfer_target now computes the general RG 96 target first, for every candidate, and overrides to the Monday after Low Sunday only when that general target itself falls after Easter Sunday -- testing the rubric's own condition directly rather than re-deriving a date-proximity rule from first principles. Confirmed against three real years the review named: 2007, 2012 and 2057 all previously sent the Annunciation to Easter + 8 (16 April, 16 April, 30 April respectively) when the correct, now-produced target is the next free day before Easter (26 March in each case -- Passion Sunday in 2007/2012, Lent III Sunday in 2057). Verified with actual CLI output for all three, before and after. Also cites RG 96 Attamen (b), the same primary-source passage, as the direct authority for All Souls' own move to the following Monday when impeded by a Sunday -- previously inferred from RG 91 entry 8's parenthetical plus the general walk, which happened to produce the right date; now stated directly. Rewrote the two existing Annunciation unit tests, whose synthetic occupants no longer trigger the (now correctly conditional) exception, and added a 2057 regression test using the real Temporal_ef.temporal as occupant plus a real-data cram pin -- both mutation-verified against the unconditional reading.
* cli: colitur day <year> dumps resolved liturgical daysLukasz Kasprzak2026-08-121-1/+66
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Assembles Rite_ef.context (lib/rites/rite_ef/rite_ef.ml[i]): temporal, anchors and vocab from Temporal_ef; rules from Precedence_ef's band, disposition and admit; season_runs = Vocab_ef.seasons; transfer_target newly implemented here. transfer_target (RG 96): the next following day that is not I or II class, with the Annunciation's own exception (Monday after Low Sunday). Terminates by a structural step bound on its internal search, independent of Calendar's own round guard, which bounds rounds across a year, not one call's walk; documented as an obligation on rite.mli's transfer_target field, which did not previously state it. Fixes the vigil-naming mismatch Task 7's review predicted: the sanctoral bootstrap names its vigils with a vigil-of-X prefix (lectio's own convention), while Precedence_ef's is_vigil only recognised the temporal cycle's own X-vigil suffix. Both are now recognised, fixing RG 91 entries 21/26 and RG 33's vigil omission for the four affected celebrations. Verified by unit test and by mutation-testing the fix (reverting it fails exactly the new rows) and against real output across several years. Suppresses data/ef/sanctoral.sexp's vigil-of-christmas via a new overlay, data/ef/adjustments.sexp: it is the same celebration as the temporal cycle's own ef-nativity-vigil, both dated 24 December. colitur day <year>: one line per civil-year day, temporal and sanctoral fully resolved through Layer, Overlay, Precedence_ef and Calendar -- the first CLI path exercising the whole Plan 3 pipeline against real data. Verified the All Souls transfer chain (Tasks 7-8-11) end to end against real output for both a Sunday year (2025, lands on 3 Nov) and a non-Sunday year (2026, observed directly on 2 Nov).
* cli: emit "-" for an absent week, keeping output column-safeLukasz Kasprzak2026-08-111-3/+3
| | | | | | | | | | | | | | | | | | | | | | | | | week is "" for roughly 30 days a year (any day outside a numbered week). Printed as-is among seven space-separated fields, that collapses two fields into a double space, so naive field-position parsing (e.g. awk '{print $4}') silently reads the wrong column on those days -- a real defect for a project whose stated design is Unix-composable CLIs. Emit "-" instead, so every line always has exactly seven single-space-separated fields. Record.headers/to_row exist for exactly this kind of column-safe encoding but use a different column set and order than this CLI's documented date/weekday/season/week/slug/rank/colour format (design spec §6); adopting them here would be a larger, unrequested format change, so this keeps the CLI's own field list and only substitutes the empty value. The cram test's pinned lines are re-verified against liturgical anchors before promoting, not blind-promoted: 1 Jan 2026 (Circumcision, class-1, white) and 2-3 Jan (ordinary class-4 Christmastide ferias, the finding-1 slug fix already re-pinned separately) are correct; 2026-04-05 remains the sole Easter Sunday line, Paschaltide week 1, class-1, white, matching the independently-computed Easter anchors printed by `colitur easter 2026` immediately above in the same file.
* rite(ef): fix duplicate slugs and omitted weeks in the temporal cycleLukasz Kasprzak2026-08-111-2/+2
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Two correctness bugs in Temporal_ef, both only visible across many years, not a single point assertion: 1. Duplicate slugs within one liturgical year. Christmastide has no numbered weeks, so the ferial fallback's <season>-<week>-<weekday> scheme collapsed every feria in it to literal week "0". Since colitur's Christmastide runs 25 Dec - 13 Jan (RG 72-73, a deliberate divergence from lectio), the same weekday recurs across that span and the keys collided (e.g. ef-christmas-0-saturday on three different dates). Fixed with a dedicated christmastide_feria_slug that splits the span into four sub-stretches, prioritising compatibility with lectio's own keys wherever lectio has one: - 26-28 Dec keeps lectio's existing ef-christmas-0-<weekday>. - 2-5 Jan becomes ef-christmas-1-<weekday> -- lectio collapses this indistinguishably into the same key as the stretch above, so there is nothing to preserve; a colitur-only lectionary gap. - 7-13 Jan splits at the actual first-Sunday-after-Epiphany origin: on/after it, this is genuinely week 1 of Time after Epiphany and takes lectio's own ef-time-after-epiphany-1-<weekday> (which also can't collide with that season's own later week-1 ferias, since it's the same computation). Before it (0-6 days, whenever Epiphany doesn't fall on a Saturday), a first attempt at labelling this "week 1" too, matching a literal reading of lectio's behaviour, was verified empirically (a throwaway sweep of the full 1583..9998 domain) to reproduce duplicates in most years -- so this remainder is its own ef-christmas-2-<weekday>, a further colitur-only gap. Verified with the same throwaway sweep: zero duplicate slugs across 1583..9998 after the fix. 2. named's week field was set by hand on some branches (Passion/Palm Sunday, Easter, Low Sunday, Pentecost and its Vigil, Christ the King) and left at None on others (Ascension and its Vigil, Corpus Christi, Sacred Heart) even though all of them sit inside a numbered season run. named no longer carries a week at all -- temporal now calls week itself for every day, named or not, so "a named day inside a run carries that run's week" holds by construction rather than by remembering to set it on each branch. temporal.mli's week field doc is reworded to state the actual rule. Covering tests: point assertions for all four Christmastide sub-stretches; a QCheck property scanning random years for any duplicate slug within a liturgical year (excluding the deliberate resumed-Sunday reuse); point assertions for the four previously-None days now carrying their week (first to fail without the fix: Ascension Vigil, expected Some 6, got None); a QCheck property asserting temporal's week equals week for every day of the year, replacing a prior property whose fallback made it structurally incapable of detecting an omitted week. Also: cite RG 91 e18 for the September/Advent Ember days matching lectio and flag the Lent/Whitsun Ember and Rogation slugs as colitur-only lectionary gaps inline, matching the existing Nativity-vigil/octave-day convention; cite RG 117/123/127/128/131 for season colours and Gaudete/Laetare rose; drop the unreachable Passiontide arm from the Sunday-rank match (Passiontide has only two Sundays and both are already named above, so no Passiontide Sunday ever reaches that fallback).
* cli: colitur temporal <year> dumps the EF temporal cycleLukasz Kasprzak2026-08-111-1/+23
| | | | | | | One line per day -- date, weekday, season, week, slug, rank, colour -- built through Record, the canonical output view. Until Plan 3 brings the differential and oracle layers, this dump is how a human checks EF temporal against missalemeum by eye.
* cli: colitur easter <year> prints Easter and its anchorsLukasz Kasprzak2026-07-311-0/+21
Argv-parsed subcommand; validates the 1583..9999 domain (exit 2 otherwise); prints 'name YYYY-MM-DD' lines. Cram test covers the output and the exit codes.