| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Owns precedence and provenance and nothing else, and never reads the
filesystem, so it is as testable as the language table.
resolve returns the value AND its source, because a setting that silently
comes from a file the user forgot about is worse than no setting at all --
config --show can then say where each effective value came from.
overlay accumulates rather than last-wins: a user has more than one.
An unknown key is reported, never fatal. A config written for a newer
colitur must still work on an older one, but silently dropping a line the
user wrote is how a typo becomes invisible.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Maps strings to strings and nothing else -- no calendars, no dates, no
filesystem. That is what lets every command use it without the kernel
learning about presentation.
Every lookup is total, and a miss returns THE KEY rather than the empty
string. A partial translation is therefore usable from its first line,
and the fully-degraded case is exactly today's output (bare slugs) rather
than a blank page.
--raw is a real identity table, not a special case threaded through every
call site: one value the whole program passes around.
Reuses Overlay_ini's INI reader rather than growing a second one that
would drift in its comment, quoting and trimming rules; parse_sections is
exposed in the .mli for that, with no behaviour change.
Fixes one defect found while running the brief's own tests rather than
transcribing them blind: weekday's internal lookup key is an English
day-name word (month's is already the numeral string), so on a miss it
echoed that word instead of the documented numeral, breaking both the
0=Sunday convention and Lang.raw's own identity contract for weekday.
weekday/month now fall back to string_of_int n directly on a miss
instead of through get's generic echo-the-search-key path; month is
byte-identical since its key already equals string_of_int n.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
pdftotext -layout of the shipped grid.ms showed only 3-4 of 7 columns and
roughly a third of each month's day numbers: tbl's plain columns (no
w()) size to their widest single entry and never wrap, so a long
fallback slug (some run past 40 characters) forced every column that
wide, the table ran far past the page, and whatever fell past the
physical edge was gone, not merely ugly. groff exited 0 throughout
(warnings, not errors), so make check-templates reported OK on a broken
artefact.
Fixed both halves. (a) The table now fits: true landscape via gropdf's
own -P-pa4l (an in-document Xpapersize=a4l escape was tried and
rejected -- it does not rotate the page in this groff), ms's own title
macro widened back out after narrowing the line length for the title
text (a second, independent way the original lost its width, found by
reading s.tmac), and every column rewritten as a genuine tbl text-block
(T{/T}, not a plain w() cell -- w() alone does not wrap, confirmed
against tbl's own generated troff code) so long, hyphenated slugs wrap
at their own hyphens instead of forcing the column wider. (b)
check-templates now captures groff's stderr per template and fails the
target if it is non-empty, rather than trusting groff's exit code.
Verified: 0 warnings (was 12), pdftotext -layout shows all 7 columns and
every day number for all 12 months (was 3-4 columns, ~12-23 of each
month's day numbers). The golden fixture is regenerated: 0 "{{", 12
month headings, exactly one Ianuarius, and each week's block (now spread
across several physical lines by the T{/T} wrap) carries exactly 6 tabs
joining its 7 cells.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The live-data CSV assertion inspected only the first field's length,
always the 10-char ISO date, which can never contain a comma -- it could
not fail no matter what emit_csv.ml did with the rest of the row.
Mutation-proved: replacing emit_csv.ml's escape_field call with identity
left every test green while the real output emitted a 14-field row
against a 13-column header. Fixed by parsing the row as RFC 4180 actually
requires (a small quote-aware splitter) and asserting the field count
matches the header, plus asserting the quoted substring appears literally.
The XML suite asserted Escape.Xml's correctness in isolation but never
that emit_xml.ml actually calls it on every interpolated value. Bypassing
one escape call at the name-element site left all tests green while real
2035 output (Sts. Fabian & Sebastian, 20 January) emitted a bare '&' that
xmllint rejects. Fixed by adding a live-data test against the 2035 fixture
asserting an escaped ampersand is present and no bare one remains.
Both new assertions were run against their named mutations and confirmed
to redden before being reverted.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Three flavours, not six: a month grid in Markdown or plain text is a
worse artefact than the booklet already is, and shipping a template we
would not use ourselves is maintenance with no reader.
These are the first templates to use weeks and in_month, so this is
where the view model earns its keep -- the booklet and the grid come
from one model with no second code path.
Every cell carries a per-cell 'last' boolean (already in the view). A
table row needs a separator BETWEEN cells and the engine deliberately
has no 'unless last' construct; the rule is shape the data, not the
template. Without it the LaTeX grid emits eight columns for seven
cells and pdflatex rejects the file.
Day cells resolve their label as {{#name}}{{la}}{{^la}}{{slug}}{{/la}}
{{/name}}, never a bare {{name.la}}: the enclosing month object has
its own name.la, and the engine's scope lookup falls back outward, so
a bare dotted lookup would render the month's own Latin name on every
day lacking one -- a wall calendar where every day reads "January".
Verified on the goldens: Ianuarius appears exactly once per file, the
month heading, never as a day label.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
LaTeX and groff are the print paths; HTML carries a print stylesheet;
AsciiDoc, Markdown and plain text are the plain-consumer paths.
AsciiDoc and Markdown use flavour none, and say so in a comment: their
metacharacters are context-dependent and escaping them aggressively
produces worse output than not escaping. The consequence is real and
documented -- a feast name containing * renders as emphasis.
Golden tests pin all six byte-for-byte for 2027. They prove the
templates RENDER, not that they TYPESET; compiling needs TeX and groff,
which is Task 13's opt-in make check-templates.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
F1: Date.add_days is UNBOUNDED (date.mli) -- only Date.make enforces
1583..9999 -- and Date.to_iso8601 pads but never truncates, so
9999-12-31's naive successor formatted as "10000-01-01", and compact
turned that into a 9-digit, non-conformant DATE on the last VEVENT of
year 9999. Confirmed at the source before fixing, and reproduced
against real `colitur emit --format ics --from 9999 --to 9999` output
(DTEND;VALUE=DATE:100000101) before touching any code.
RFC 5545 section 3.6.1: a VEVENT with a DATE-valued DTSTART and
neither DTEND nor DURATION has an implicit one-day duration, so
omitting DTEND for that one event is the standard's own correct
answer, not a workaround. dtend_of re-derives the successor's
year/month/day and re-validates them through Date.make -- the one
function that actually enforces the domain -- before trusting the
string; None means the caller omits the DTEND line entirely.
F2 (minor, same function): documented next_day's own Error branch as
dead-but-silent on shipped data (event's iso <> "" guard is the only
caller and always parses) -- behaviour unchanged, comment only.
Two new tests: the domain's last VEVENT (DTSTART 99991231) has no
DTEND line at all; every DTEND anywhere in a 9999 feed is exactly 8
digits (the general form of the bug, catches a regression anywhere
else in the domain too). Existing 2027/2028 DTEND-arithmetic
assertions untouched and still pass.
Mutation-proved: both new tests fail against the pre-fix code
(9-digit DTEND value caught verbatim), pass after.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Not a template job: folding, escaping, exclusive DTEND and stable UIDs
are rules a logic-less template cannot enforce, and each fails silently
in a subscriber's client rather than loudly at generation.
DTEND is EXCLUSIVE for an all-day event (section 3.6.1). Wrong here
shows every event a day short, everywhere.
UIDs are YYYYMMDD-<rite>@colitur and stable across regenerations
(section 3.8.4.7). Wrong here duplicates the whole year in every
subscriber's phone, months later.
Every line is CRLF-terminated and folded at 75 octets (section 3.1).
No RRULE: a liturgical calendar is not a recurrence rule. Asserted, so
nobody optimises it later.
DTSTAMP is a parameter, not a clock read. RFC 5545 requires it and the
obvious implementation reads the wall clock -- which violates the
kernel's determinism rule and would make two feeds from identical data
differ byte-for-byte, defeating reproducible builds and any reviewable
diff on a published tree.
Corrected one test literal against real engine output: DTSTAMP is a
per-VEVENT property (section 3.8.7.2), not calendar-level, so the
default-value line count is 365 (every event), not 1.
Mutation-tested: a non-exclusive DTEND reddens the suite.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Element-per-field; attributes carry identity only and there is no mixed
content, so a consumer's XPath never has to distinguish the two.
Schema validation is an opt-in make check-schema via xmllint, not an
in-suite assertion: validating XSD needs an XML library and the
dependency list is frozen. It prints SKIPPED loudly when xmllint is
absent, because a silent skip reads as a pass. The suite asserts
well-formedness properties directly instead.
This corrects the design spec, which claimed in-test validation.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Both consume the VIEW, not the kernel, so every emitter and every
template describe exactly the same fields -- there is one vocabulary,
not five.
CSV is RFC 4180: a field with a comma is quoted. That is live on real
data, not hypothetical -- 'St. Joseph, Spouse of the Bl. Virgin Mary'
would otherwise split into two columns.
JSON is hand-rolled because the dependency list is frozen and escaping
is the only subtlety. Control characters below 0x20 are \u-escaped per
RFC 8259 section 7.
There are no numbers in the view, deliberately: a consumer never has to
guess whether week is 2 or "2".
schema/day-v1.json pins the shape. Once a phone subscribes or a site
fetches this, it is a promise to strangers -- adding a field is minor,
renaming one means /v2/.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Shapes a civil year of resolved days into the value a template renders
against. This layer is why the engine can stay logic-less: a month grid
needs leading blank cells, week bucketing and an in-month test, and a
logic-less template can compute none of it.
Both weeks and days are offered at every level -- the booklet walks
days, the grid walks weeks -- so the two artefacts cannot drift.
Colours are six booleans, not hex: hex bakes a presentation policy into
the engine, and LaTeX, groff and HTML each want a different colour
expression. Asserted: exactly one of the six is true on every day of a
whole year, so a template keying off them can never get none or two.
Padding cells carry every field a real day carries, empty, so a template
never hits a missing key mid-grid.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Every interpolated value is escaped for the template's flavour; the
template's own literal text never is, because that is the author's
markup. There is no raw form, so a template cannot opt out.
Scope is a stack with outward fallback, so a grid template can reach the
year number from inside a week without the view duplicating it into
every cell.
A missing key renders empty -- the one deliberate silence, so a template
survives a rite that does not set every optional field.
Mutation-tested: dropping the Escape.apply call reddens the
data-cannot-escape-flavour case.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
F1: test_no_raw_or_partial_form's first assertion only excluded one
literal shape (Ok [Var ["{name"]]), so it could not actually catch a
future raw/unescaped constructor under a different name. Replace it
with an assertion of the real parse result for {{{name}}}
(Ok [Var ["{name"]; Text "}"]), documented behaviour rather than a
guarantee this test cannot check -- the real guarantee is structural:
node has exactly four constructors and none of them is raw.
F2: {{.}}, {{#}}, {{^}} and {{/}} used to parse to a Var/Section/
Inverted with an empty path, reachable but never designed. This engine
has no "current context" for a bare dot to mean, so a bare-dot or
empty-sigil path is now a parse error at lex time, covering all four
sigil forms via one path helper. The existing "empty tag {{}}" branch
is unchanged and still reachable (a fully empty body is a distinct
case from a sigil with an empty path).
|
| |
|
|
|
|
|
|
|
|
|
| |
Placeholders, sections, inverted sections, comments. Nothing else: no
partials, no lambdas, no expression evaluation, no raw form. A template
is data, never a program, which is what keeps an untrusted template safe.
Errors rather than silence on a malformed template: an unterminated tag,
an unclosed section, a mismatched close and a partial all return Error.
Swallowing '{{name' as text is how a typo becomes invisible missing
output in a printed booklet.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
fold_ics's UTF-8 backoff loop could back `cut` all the way down to
`pos` on 74+ consecutive continuation bytes (0x80-0xBF), producing a
zero-length chunk and recursing on the identical position forever --
not producible by valid UTF-8, whose longest continuation run is 3,
but the kernel's own totality requirement covers arbitrary octet
strings, not only valid ones. When backoff finds no boundary inside
the window, cut hard at the limit instead, so forward progress is
unconditional.
test_fold_never_splits_utf8 previously asserted only that unfolding
reproduced the original bytes, a property folding preserves at any
cut position and therefore blind to a boundary violation. It now also
asserts the named property directly: no continuation chunk may start
with a UTF-8 continuation byte. A new regression test feeds fold_ics
100 consecutive continuation bytes and asserts it terminates with
every line at or under 75 octets.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Six flavours: latex, groff, html, xml, ics, none. Markdown, AsciiDoc and
plain text map to none deliberately -- their metacharacters are
context-dependent and escaping them aggressively produces worse output
than not escaping.
An unrecognised extension returns None rather than falling back to none:
guessing the flavour wrong produces malformed output that looks fine
until it does not.
Folding backs off to a non-continuation byte, so a fold never splits a
UTF-8 sequence -- the failure mode that would corrupt Polish and Latin
names in a published feed.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
lectio adopted the last two Missal corrections colitur argued:
C14 3 -> 0 RG 96(a)/97/98. lectio resolved each impeded I-class feast's
transfer independently, so in 2008, 2035 and 2046 St Joseph and
the Annunciation both walked onto the Monday after Low Sunday
and Joseph, losing there, was observed on no day of those years
at all. It now resolves the year's transfers as a set: the
Annunciation takes the Monday as its sedes propria, Joseph the
Tuesday.
C15 7 -> 0 The Holy Family propers' own 13-January rubric, "sine
commemoratione Baptismatis D.N.I.C.". lectio kept the Baptism
as the observed office on all seven such years in 2005-2050.
Those were the last two BEHAVIOUR classes. What is left is C1 (138) and C6
(119), both pure slug vocabulary.
Measured across the whole fixture, applying only the season normalisation the
test already applies: 579 of 16801 days differ, and every one of them differs
on the SLUG ALONE. Zero days differ on season, rank, colour, Epistle or Gospel.
(The test's own norm_slug table already maps 322 of the 579; the 257 left are
C1 and C6. The field profile is the same either way.)
So the two engines now agree on every liturgically meaningful field on every
day of 2005-2050. The residue is what the two projects call things --
ef-christmas-2-friday against ef-time-after-epiphany-1-friday for the same day,
same Mass. Aligning it means renaming lectio's slugs, and those are keys: its
lectionary, clectio's generated tables and any user overlay are built on them.
Left as vocabulary rather than forced.
Fixture refreshed against lectio e713da2; 13 rows changed, colitur agrees with
all of them. 24 divergence classes at the start of this work, 2 now.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The ferias between Epiphany and the first Sunday after it repeat Epiphany's
own Mass in lectio now; they had been taking the Mass of the Sunday that
follows them.
C31 98 -> 0
C1 40 -> 138 grew, and changed character
C1 is now SLUG VOCABULARY ONLY, and its note says so. Season, rank, colour and
both citations agree with colitur on every day of the January window; what
differs is the identifier -- ef-christmas-2-thursday against
ef-time-after-epiphany-1-thursday for the same day with the same Mass. C6 is
the same shape.
That is 257 of the 267 remaining rows. Aligning the vocabularies would mean
renaming lectio's slugs, and those are keys: its own lectionary, clectio's
generated tables and any user overlay are built on them. Recorded as
vocabulary rather than closed, because the rows do differ -- just not in
anything a reader of either engine's output sees.
Measured for 2026: season, rank and colour agree on all 365 days; 14 days
differ and every one differs on the slug alone.
5 classes -> 4, agreement 98.4%.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
lectio carries the readings of the nine Common-routed saints and of three more
that had proper Masses but no reading at all. Every day of 2005-2050 now
resolves a reading there; four days used to come back empty.
C38's own note said it could not close without lectio gaining a Commons
concept. The readings arrived instead of the abstraction, which was enough:
each Common resolves to one first/gospel pair for the saint that names it, so
a table plus assignment layer would have bought indirection and nothing else.
C20 14 -> 0
C38 70 -> 0
7 classes -> 5, 351 rows -> 267, agreement 97.9% -> 98.4%.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
lectio gained the Passiontide week split and thirteen lectionary entries.
C23 230 -> 0 Holy Week's own six Masses
C32 99 -> 0
C35 60 -> 0 the first weeks after Epiphany and Pentecost (RG 299)
C33 46 -> 0 Corpus Christi
C34 46 -> 0 the Sacred Heart
C26 43 -> 0 Passion Tuesday
C19 7 -> 0 the Holy Family week-index residue
C1 75 -> 40
C31 63 -> 98 GREW
The Passiontide split is the one that carried most of this. efWeek had no
Passiontide case, so both weeks numbered 0 and every day of Holy Week took the
slug of its Passion-week namesake -- one set of readings for two weeks with
entirely different Masses. Good Friday was reading Passion Friday's.
C31 growing is the same bookkeeping as C1's earlier growth and is recorded in
its own note: C32 and C35 closed, and rows that carried several causes now
carry only this one. Nothing new disagrees.
17 classes -> 10, 1019 rows -> 488, agreement 93.9% -> 97.1%.
What remains is almost entirely NAMING rather than disagreement: C6 (119) is
colitur's ef-nativity-octave-day-N against lectio's ef-christmas-N-weekday on
days where both now read the same Mass, and C1 (40) is the same shape in the
January window.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
lectio gained Rogation Monday and Tuesday (RG 87, violet under RG 128(d)) and
the Missal's own Mass for the weekdays within the Octave of the Nativity
("Diebus infra octavam Nativitatis Domini", Titus 3:4-7 / Luke 2:15-20).
C8 26 -> 0 CLOSED
C6 119 -> 119 narrowed: the cause halved, the count did not
C6 is worth reading carefully rather than skimming the number. Its rows no
longer differ on First_f or Gospel_f at all -- the two engines now read the
SAME Mass on 29-31 December, where lectio previously served the Sunday's. What
is left is Slug_f alone, colitur's ef-nativity-octave-day-N against lectio's
ef-christmas-N-weekday. That is vocabulary, not disagreement. The count did not
move because the entry's gate already admitted a Slug_f-only subset, which is
exactly the case where a row count is a misleading summary.
17 classes, 1019 rows, agreement 93.9%.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
lectio built the votive Office of Our Lady on Saturday (RG 78) and then said
RG 309(a)'s own Mass for it. Both halves were needed: RG 78 keeps the office
and gives it white, RG 309(a) picks among the Missal's five seasonal
formularies. Fixing only the first leaves the day announcing Our Lady and
reading the feria's Mass -- the bug colitur itself shipped briefly and fixed
in its v0.3.0.
C17 439 -> 0 CLOSED -- the largest class between the two engines
C31 103 -> 63 Saturdays that carried both causes; only this one left
C35 69 -> 60 same
C1 35 -> 75 GREW
C1 growing is not a regression and is recorded as such in its own note: the
January Saturdays C17 used to absorb now differ only on the slug vocabulary
C1 already covers, so they land there instead. Rows moved between entries as
the larger cause was removed.
Also in this refresh, from the same session: weeks after Epiphany now count
from the first Sunday after it rather than from 6 January, which took C19
from 35 to 7 -- what remains there is the Holy Family interaction, a different
cause needing RG 17(b) in lectio.
19 classes -> 18, 1493 rows -> 1045, agreement 91.1% -> 93.8%.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
lectio has taken three further corrections colitur argued from the Missal: the
twelve RG 124 colours (red is for Apostles, Evangelists and Martyrs, and
missalemeum had the rule inverted in both directions), Good Friday's black
under RG 128(b) with RG 132, and RG 72's Christmas Time boundary at 13 January
inclusive.
864 rows changed. Two classes closed outright and two moved:
C18 450 -> 0 the RG 124 colours -- the LARGEST single class in this file
C36 46 -> 0 Good Friday's colour
C1 159 -> 35 the season boundary stopped differing
C23 184 -> 230 Good Friday's rows fold back, the split no longer needed
C36 is closed rather than kept at zero because the split it represented has
dissolved: it existed only to hold Good Friday apart while its diff set had a
third member, and with the colour agreeing the rows are the exact pair C23 has
always gated on. Restoring 230 is not a regression, it is the number returning
to what it was before the split.
Cited classes 24 -> 19, divergent rows 2252 -> 1521, agreement 86.6% -> 90.9%.
The percentage is again the least interesting part. C18 alone was 450 days on
which the two engines agreed about nothing and now agree because both apply
RG 124 the same way round.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Every previous refresh of this fixture carried lectio's answers TO colitur,
because colitur's data was bootstrapped from lectio. This one carries
colitur's answers BACK. lectio has merged eight field corrections colitur
argued from the 1962 Missal: ubaldus and didacus promoted from commemoration
to III-class feast on the Missal's own calendarium, both August vigils
recoloured violet under RG 128, and both Ember Saturdays' readings, which
lectio had carrying St Thomas's and St Matthew's Masses respectively.
227 of the 16801 rows changed. Three cited classes went to zero and are
CLOSED and REMOVED, citations preserved in the register, the same discipline
C2-C5, C7, C9-C13, C21, C22 and C25 already record:
C27 40 -> 0 the Advent Ember Saturday's readings
C28 40 -> 0 the September Ember Saturday's readings
C37 77 -> 0 the two vigil colours
C37 is worth noting by name: its own note called the shared lineage "the
sharpest demonstration in the project", colitur and lectio and missalemeum all
carrying the same two wrong colours. Two of the three now carry the right one.
C38 NARROWED rather than closed, and the distinction matters. Its identity
half is gone -- lectio ranks both saints class-3 now -- but the same 70 rows
still differ on readings, because both saints take their Mass from a Common
and lectio has no Commons concept to resolve one. It is gated on
diffs = [First_f; Gospel_f] exactly, which is C18's shape. It cannot close
without lectio gaining a capability, which is a feature rather than a fix.
Net: 24 cited classes to 21, 2252 divergent rows to 2095, agreement 86.6% to
87.5%. The number that matters is not the percentage though -- it is that
those 227 days now agree BECAUSE BOTH ENGINES FOLLOW THE MISSAL, where before
they agreed because both inherited the same errors from Divinum Officium.
That is the failure mode CLAUDE.md's lineage note describes, resolved rather
than documented, for the first time in this project.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A user-supplied overlay is the only sexp this engine reads that a human writes
by hand, and two of Celebration.t's eight fields carry nothing such an author
can meaningfully supply: citations is always empty for a local feast, since
citations come from the rite's lectionary and never from calendar data, and
layer merely repeats the overlay file's own id.
Requiring both made the commonest first mistake -- omitting them -- fail with
'lib/kernel/celebration.ml.t_of_sexp: the following record elements were
undefined: citations layer', which names a source file the author will never
open and does not say what to write instead. A minimal local feast needed 12
lines, two of them noise.
Overlay.load now fills each field only where ABSENT, so an explicitly stated
value always wins: an overlay may legitimately name a layer different from its
own id, and defaulting must not silently overwrite that. A test pins both
directions.
Deliberately scoped to overlays. Layer.load, which reads the shipped
sanctoral, is untouched and stays strict -- that data is the project's own,
every field of it is asserted by tests, and a missing one there is a defect
rather than a convenience.
Parse failures also stop naming kernel source paths: the five prefixes that
actually reach a user are rewritten into the vocabulary of the file being
edited, and anything unrecognised passes through verbatim rather than being
reworded into something possibly wrong.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
"113. Commemoratio de Tempore fit primo loco. In admittendis et ordinandis
aliis commemorationibus, servetur ordo tabellae praecedentiae." Only the
second sentence was implemented, adopted on ef-rg16a as the admission and
ordering criterion. The first -- the commemoration OF THE SEASON is made in
the first place -- was not, so ordering ran through band alone for every
commemoration including the seasonal one.
Reconciled with RG 110(c), which adds the inseparable Peter/Paul pair "ante
omnes alias commemorationes". Both rubrics are primary-source certain: the
photographic scans' OCR is illegible at RG 110(c), but docs/research/LT.txt,
the electronic transcription, carries the Rubricae Generales complete, and
O'Connell's footnote 39 turns out to render that clause exactly rather than
gloss it. They share a technical term. RG 113's own sentence fixes the sense
of "aliae commemorationes" as the ones other than the de Tempore one it has
just placed primo loco, and RG 110(c) sits three paragraphs earlier in the
same code. Read consistently, the pair heads the OTHERS, not the whole list:
season, then the pair, then the table order. So this runs after
rg110_additions, and List.partition's stability keeps the pair adjacent while
the seasonal commemoration steps in front of it.
Blast radius, two full 1583-9999 sweeps diffed: 3 533 days, every one
ORDER-ONLY -- the commemoration set is identical on both sides of every
changed day, checked as sorted multisets, zero days where anything else
moved. All are 22 February with chair-of-st-peter observed. Exactly the
population measured before the rule was written.
The order-sensitive test row is in test_precedence_ef.ml's admit_cases, which
is still the only place in the suite that asserts commemoration order at all.
It needed a mixed-origin candidate: every other candidate in that table is
origin = Temporal by default, so the rule is an identity on them.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Both carried rank Class3 with status Commemoration_only, bootstrapped from
lectio, which inherits it from missalemeum, which is generated from Divinum
Officium. The Missal's own universal calendarium ranks both "III classis"
outright, with no commemoration rubric: "S. Ubaldi Ep. et Conf., III classis"
(16 May) and "S. Didaci Conf., III classis" (13 November), corroborated in
the second scan and in a published 1962 Ordo.
Universal by construction, not by assumption: the calendarium carries no "pro
aliquibus locis" marker in its 573 lines while the wider Missal carries 79 of
them elsewhere. Each saint also has his own Mass entry in the Proprium, which
a bare commemoration never has -- a commemoration carries an oration only.
Found by auditing ALL 290 fixed-date entries against that calendarium, day by
day, anchored on the Roman calends column because the arabic day column is
OCR-wrecked and the dominical-letter column vanishes in some months. These
two were the only status defects in the whole file. The 15 days the automated
pass could not cover were read by hand and all agree.
Neither needed a proper authored: each takes its Mass from a Common with only
its own Oratio proper, per its own rubric at its own date. ubaldus shares
patrick's Common exactly. didacus needed one newly transcribed --
common-of-a-confessor-not-a-bishop-2, the "Altera Missa" Iustus, 1 Cor 4:9-14
and Luke 12:32-34, read independently in both scans, which agree, so the
page-image third reading this file's method requires on disagreement was not
triggered.
Blast radius, full 1583-9999 sweep, every day classified: 13 036. That is
5 564 ubaldus observed, 7 196 didacus observed, and 276 where the Ascension
Vigil still wins on 16 May but its single RG 111(c) slot passes from
rogation-wednesday to ubaldus -- correct and already pinned, since the
Rogation days are the MINOR Litanies and so an ordinary commemoration, which
RG 113's table order ranks below a III-class feast. Cited as C38 (70 rows)
and M30 (3); C8, C17 and M18 shrink accordingly, each with the reason
recorded rather than the count silently adjusted.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
RG 128, transcribed in the rules register since 2026-08-11, gives violet to
"vigils of II and III class outside Paschaltide". The Vigil of the Assumption
(14 August, II class) carried White and the Vigil of St Lawrence (9 August,
III class) carried Red. Both are outside Paschaltide, so both are violet. The
Ascension's vigil is untouched: it is the one II-class vigil inside
Paschaltide and was already correctly white.
The rule had been sitting in the register, disagreeing with the shipped data,
since before the data was bootstrapped. It was invisible because colitur,
lectio and missalemeum all carried the same two wrong colours -- colitur was
bootstrapped from lectio, lectio's ini is generated from missalemeum, and
missalemeum uses Divinum Officium's data files. Three sources agreeing is one
source counted three times, and the differential and the oracle both went
green on the shared error. It surfaced only against witnesses outside that
tree: a published 1962 Ordo flagged the Assumption's, and O'Connell, The
Celebration of Mass 4th ed. (1964), section 4(c) and footnote 169, gave the
general rule and with it St Lawrence's, which the Ordo omits entirely.
Two golden pins added, in years where each vigil is actually observed rather
than impeded. The divergence this creates against every DO-lineage source is
cited in both allow-lists: C37 (lectio, 77 rows -- 92 possible minus 15 where
the vigil falls on a Sunday) and M29 (missalemeum, 3 rows in 2026-2027 and 2
in each of 2035 and 2038). Both gated on the exact slugs or dates and on
colour alone, so any other divergence on those days would still surface.
Also recorded, no code change: rose on Gaudete and Laetare stays. RG 131 is
permissive ("adhiberi possunt") and the Ordo prints violet, so the colour
field carries a permission rather than a requirement on those two Sundays a
year -- a decided position now, not an unexamined one.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
RG 128(b)'s own exception list, transcribed in the rules register and
primary-source-verified since Task 16, excepts "Actione liturgica feria VI
in Passione et Morte Domini usque ad Communionem exclusive" from the violet
that otherwise runs from Septuagesima to the Easter Vigil. RG 132 assigns
black there. The register recorded this as an acknowledged gap rather than a
disputed reading -- the code comment beside the Holy Thursday exception says
so in as many words -- and it is closed here.
Found by two witnesses outside the Divinum Officium lineage that colitur,
lectio and missalemeum all share: O'Connell, The Celebration of Mass, 4th ed.
(1964), revised throughout to the Codex Rubricarum (1960) and the 1962
Missal, and a published 1962 Ordo. Both say black. So does missalemeum's own
colour set, which orders the day "bv".
The rubric is per-action ("usque ad Communionem exclusive") and the model
emits one colour per day; black is the day's principal one, the same
acknowledged limit RG 126's Palm Sunday blessing already carries.
Blast radius: one day per year, colour only, across the whole 1583-9999
domain. Two golden pins re-pinned. Good Friday's 46 differential rows leave
C23 for a new C36 gated on the exact triple [First_f; Gospel_f; Colour_f],
so C23's own exact-pair gate still means what its note says for its
remaining four days: 184 + 46 = 230, no residue, both counts from the
comparator rather than hand-counted.
|