diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-03 21:13:38 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-03 21:13:38 +0200 |
| commit | 81a9df4bab615abdd8ef0e2fe7322219ce00ee6d (patch) | |
| tree | 82e358bc2e7f95c7133481e6a5f6a347dbc605c5 | |
| parent | d0807440309e2357d51cd56c72a443072b0ba57f (diff) | |
| parent | fe136f0952284b9a339252926dfd4675c4a7c3c6 (diff) | |
| download | colitur-b07c2e5f2c4394806a33f59f0046f78ac9a800da.tar.gz colitur-b07c2e5f2c4394806a33f59f0046f78ac9a800da.zip | |
The OF's readings had never been validated against anything but the lineage
they arrived on. Pointing a second lineage at them found ten corrupt
references on the first run.
| -rw-r--r-- | CHANGELOG.md | 6 | ||||
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | bin/main.ml | 411 | ||||
| -rw-r--r-- | colitur.opam | 2 | ||||
| -rw-r--r-- | data/of/expected-divergences-litcal.sexp | 28 | ||||
| -rw-r--r-- | data/of/lectionary.sexp | 22 | ||||
| -rw-r--r-- | dune-project | 2 | ||||
| -rw-r--r-- | man/colitur.1 | 4 | ||||
| -rw-r--r-- | test/cli.t | 29 | ||||
| -rw-r--r-- | test/dune | 9 | ||||
| -rw-r--r-- | test/test_lectionary_of.ml | 80 |
11 files changed, 200 insertions, 399 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 73421b7..a8cc741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ One line per release. Versions are git tags (`vX.Y.Z`). +## [1.2.1] - 2026-09-03 +- **Ten corrupt reading references in the OF lectionary, every one in a Year II entry.** Nine ended at verse 22 where the OLM ends them at 20 (`Mark 1:14-22`, `Mark 4:1-22`, `Mark 5:1-22`, `Matthew 7:15-22`, `Matthew 17:14-22`, `Matthew 18:15-22`, `1 Samuel 1:9-22`, `1 Samuel 3:1-10,19-22`, `James 5:13-22`); the tenth read `Matthew 17:9a,10-135`, a verse Matthew 17 does not have. Not one Year I entry was affected -- that distribution is what makes this a transcription fault rather than ten independent slips. Eight corrections are confirmed **verbatim in the Latin OLM 1981**, the primary source rather than a witness; the two the OCR does not yield are impossible on their face, since James ends at 5:20 and Matthew 17 has twenty-seven verses. Corrected **upstream in lectio** and this calendar regenerated from it, per the data file's own instruction not to hand-edit a generated artifact -- the diff is exactly the source SHA-256 pin plus the ten references. +- **Found by building the readings validation the OF had never had.** The litcal layer compared season, grade and identity and stopped there, so every reading colitur served had been validated by nothing except the lineage it arrived on. Two detections, neither sufficient alone: an **internal invariant** needing no external data at all -- in Ordinary Time the OF weekday Gospel does not depend on the year of the cycle, OLM n. 69 point 4 alternates only the First reading, so a `-i`/`-ii` pair whose Gospel differs is a defect by construction -- which catches six; and the **litcal comparison**, which catches three of those plus the four the invariant structurally cannot see, being First readings and an Advent entry that no such symmetry constrains. The invariant is scoped to Ordinary Time deliberately: unscoped it reports seventeen further pairs in Advent 4 and the Christmas octave, all of them correct, since those weekdays are keyed to the calendar date. Mutation-tested, and worth recording how: a first attempt changed *both* halves of a pair and passed for the wrong reason. Measured, the 20-vs-22 divergence class against litcal goes 26 to 0. +- **`--help` is one screen, not the manual.** It printed 374 lines -- every output format, the RFC 4180 quoting rule, the template scope hazard, the publish manifest semantics. All of it real documentation, all of it already in `colitur(1)` beside the binary. A help text nobody reads at the terminal is not more helpful than a short one, it is less, because the reader stops looking. Now 42 lines, and each dropped section was checked to have a man-page counterpart rather than assumed to. One check moved rather than died: `cli.t` pinned `rubrics`' documented field list against what it actually prints, because that count drifted once and stayed wrong for a whole review round; it now reads the worked example out of the man page, which `test/dune` gains as a cram dep. +- Also: `L6`/`L7`, the Sacred Heart / John the Baptist tie at an equal Tabula entry, gains the citation its own note said did not exist -- Tabula I.3 enumerates *"Sollemnitates Domini, beatae Mariae Virginis, et Sanctorum"*, an order rather than a bare category, which read that way settles the tie against colitur's alphabetical answer. Recorded with the counter-argument (entry 4 marks its internal order with explicit lettering; entry 2's list cannot be an ordering at all) and with what would settle it. Verdict stays open and behaviour is unchanged. 917 tests. + ## [1.2.0] - 2026-08-28 - **Reading the output yourself, rather than through `awk`.** `--pretty` draws each day as its own box instead of a row: a heading with the date and the liturgical colour, then the celebration, its rank and season, and any commemorations, each on its own line. The box art is **pure ASCII** -- only `+`, `-` and `|`, never Unicode box-drawing -- because this format exists to be pasted or piped into a document, a mail or a plain-text ordo, and U+2500 and its relatives survive that only when every stage agrees about encoding and font; alignment counts UTF-8 code points rather than bytes, so a name carrying a ligature still lines the right edge up. Colour is written only when stdout is a terminal, `NO_COLOR` is honoured on presence whatever its value (the convention's own rule -- reading it as a boolean is how tools get it wrong), and the colour column degrades to its initial rather than simply vanishing when piped. Nothing should be written to parse it: its layout is free to change, which is precisely what the default format is not. - **`--month N`, `--date YYYY-MM-DD` and `--today`** narrow a report to part of a year. They matter most under `--pretty`, whose boxes span seven lines and so survive no line-wise `grep` at all -- the nearest equivalent is a paragraph-mode `awk` incantation that works only because the boxes happen to be blank-line separated -- but they are **independent** of it and narrow the default format too. The three are **alternatives, not a stack**: naming two is an error rather than a silent win for one. `--date` and `--today` NAME a year, so on those two the year may be omitted (`colitur day --today` is a complete command) and must agree when given as well, the same rule the positional year and `--year` already follow; the disagreement message names the flag, because with `--today` the second year appears nowhere on the command line for the reader to find. `--month` names no year and so still needs one. `temporal` refuses `--year` yet accepts `--date`/`--today` including as its year source, which is not an inconsistency: `--year` is a second spelling of the positional year, which `temporal` deliberately does not offer, whereas `--date` selects a DAY and merely happens to determine which year contains it. @@ -152,9 +152,9 @@ sections, `lang/traditions.ini` in full, and all three `--sigla-*` flags. ## Status Releases through `v1.0.0` are EF only; `v1.1.0` added the OF module as a -peer rite (see above), and `v1.2.0` adds `--pretty` and the `--month`/ -`--date`/`--today` narrowing flags. -**916 tests**, of which the property suite runs against every year in +peer rite (see above), `v1.2.0` adds `--pretty` and the `--month`/`--date`/ +`--today` narrowing flags, and `v1.2.1` corrects ten OF reading references. +**917 tests**, of which the property suite runs against every year in 1583-9999 rather than a sample — that is how confidence extends past the ~2050 horizon of any reference to check against. The six layers below describe the EF's own validation specifically; the OF's own (also diff --git a/bin/main.ml b/bin/main.ml index 7565090..ecc763e 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -1984,383 +1984,60 @@ let publish_report ~rite ~lang ~sigla ~from_y ~to_y ~out ~overlays ~dtstamp ~pru same reason. Deliberately NOT embedded in [help_text]: the cram test pins help's first line, and a version in it would make every release edit a test expectation for no gain. *) -let version = "1.2.0" +let version = "1.2.1" +(* --help is a REMINDER, not a reference. It answers "what are the commands + and which flag do I want", in one screen; every rule, format, hazard and + worked example lives in colitur(1), which is installed beside the binary + and is where a reader who needs more should be sent. + + It used to print 374 lines -- the whole manual, including the RFC 4180 + quoting rule and the template scope hazard. A help text nobody can read + at the terminal is not more helpful than a short one; it is less, because + the reader stops looking. *) let help_text = - {|colitur -- deterministic liturgical calendar engine (Roman rite, 1962) + {|colitur -- deterministic liturgical calendar engine (Roman rite, EF 1962 and OF) usage: - colitur easter <year> Easter, and the movable feasts anchored to it - colitur temporal <year> [--rite ef|of] [--pretty] - [--month N | --date YYYY-MM-DD | --today] - the temporal cycle, one line per day - colitur day <year> the resolved day identity, one line per day - colitur readings <year> the Mass reading citations, one line per day - colitur rubrics <year> the Mass formulary said, one line per day - colitur day|readings|rubrics [<year>] [--year Y] [--rite ef|of] - [--overlay FILE ...] [--lang CODE|FILE] [--raw] [--pretty] - [--month N | --date YYYY-MM-DD | --today] - <year> may be given positionally or as --year (both, if they - agree); rubrics's own --lang/--raw govern its trailing - formulary-name column, --sigla-* stay refused there (see - "naming" below); --pretty and the narrowing flags are described - under "reading it yourself" below - colitur emit --format csv|json|sexp|xml|ics --from Y --to Y [--rite ef|of] - [--overlay FILE ...] [--dtstamp S] [--lang CODE|FILE] [--raw] - render a resolved year range through one of five emitters - colitur table [<year>] [--year Y] --template FILE [--rite ef|of] - [--flavour X] [--overlay FILE ...] [--lang CODE|FILE] [--raw] - colitur render --template FILE [<year>] [--year Y] [--rite ef|of] - [--flavour X] [--overlay FILE ...] [--lang CODE|FILE] [--raw] - compute year Y and render it through FILE, a logic-less - Mustache-family template; table and render are the same - operation, two names (see "rendering" below) - colitur publish --from Y --to Y --out DIR [--rite ef|of] - [--overlay FILE ...] [--prune] [--dtstamp S] - [--lang CODE|FILE] [--raw] - write the static tree: per-year csv/json/xml/ics, one JSON - file per day, the schema and a generated index (see - "publish" below) - colitur lang --list which language files this build can find - colitur lang --dump CODE a language's full key set, in INI form, on stdout - colitur lang --check FILE what a language file is missing, and any typo'd key - colitur config --show every setting, its value and where it came from - colitur new-overlay print a starter overlay file to stdout - colitur convert FILE.ini flat INI overlay -> S-expression, on stdout - colitur check FILE ... load an overlay, say what it does, exit 2 if not - colitur -h, --help this help - colitur -V, --version print the version and exit - -<year> is a civil year, 1583..9999 inclusive. Each report covers 1 January to -31 December of that year, not a liturgical year. `emit`/`publish` take a -RANGE instead (--from Y --to Y, inclusive) and do not also accept a single ---year or positional year -- deliberate: they may compute many years in one -run, and a third, single-year spelling on top of the range form would add -parsing surface for no real workflow gain. - -reading it yourself: - --pretty lay the rows out as boxes for a person rather - than for awk; accepted on day/readings/rubrics/ - temporal, refused elsewhere. The box format is - for eyes only and may change between releases -- - parse the default rows, which will not. - --month N print only that month, 1..12 - --date YYYY-MM-DD print only that day - --today print only today - - The three narrowing flags are ALTERNATIVES -- naming two is an error, not a - silent win for one. They work in the default format too, and matter most - under --pretty, whose boxes span several lines and so survive no line-wise - grep at all. - - --date and --today NAME a year, so on those two the year may be omitted: - `colitur day --today` is complete. Give one anyway and it must agree, the - same rule a positional year and --year already follow. --month names no - year, so it still needs one. On `temporal`, which refuses --year, --date - and --today may still supply the year: they select a day and merely happen - to determine the year, which --year does not do. - -output formats: - day date weekday season week slug rank colour [+commemoration ...] [name] - 2026-04-05 sunday paschaltide 1 ef-easter-sunday class-1 white - readings date slug | Epistle | Gospel [| name] - 2026-12-25 ef-nativity | Heb 1:1-12 | John 1:1-14 - rubrics date, formulary slug, source, creed, gloria, preface [name] -- TAB-separated - 2026-01-01[TAB]ef-circumcision[TAB]own[TAB]true[TAB]true[TAB]nativity[TAB]In Octava Nativitatis Domini - - A citation contains spaces, so readings uses " | " between its fields while - day stays space-separated; that is why they are separate commands rather - than extra columns. The same reasoning is why the resolved display NAME, - present by default (see "naming" below), is appended as the LAST field - rather than substituted for the slug shown above: a name may itself - contain spaces, and inserting it earlier in the row would break every - fixed-position field that follows it. It is present only when it differs - from the slug already shown -- under --raw, or any language with no entry - for that particular day, the trailing field is simply absent, which is - what makes --raw byte-identical to this program's pre-naming output. - - rubrics prints which Mass is actually said and how that was decided - (source: proper/own/preceding-sunday/common/votive) -- not always the - day's own: a weekday with no proper resumes the preceding Sunday's, a - saint with no proper says his assigned Common -- followed by whether the - Creed is said (RG 475-476), whether the Gloria in excelsis is said - (RG 431-432, deferring to the Breviary's own Te Deum rule, nn. 237-238, - for RG 431(a)), and which preface is said (RG 482-499) -- Creed/Gloria - both "true"/"false", OCaml's own literal, not "yes"/"no" or "1"/"0"; - preface one of nativity/epiphany/lent/holy-cross/easter/ascension/ - sacred-heart/christ-the-king/holy-spirit/trinity/bvm/st-joseph/apostles/ - common/requiem, or "-" when this engine resolves no Mass at all that day - (Good Friday). TAB-separated rather than space or " | ": the resolved - formulary NAME (below) can carry both spaces and punctuation a citation - never does, which rules out either alternative already in use above. - --overlay is accepted (the observed celebration it changes decides the - formulary, the Creed, the Gloria and the preface); --sigla-* are refused - -- this row prints no citation for them to affect. - - --lang/--raw ARE accepted (unlike --sigla-*): `said` (the formulary slug) - is a machine key exactly like `day`'s own slug, and this row resolves it - to a display name under the identical append-only rule day/readings use - -- present as a trailing 8th field only when it differs from the slug - already printed, so --raw (or a language with no entry for that day) is - byte-identical to the seven-field row shown above. - - emit one schema (season, week, slug, rank, colour, subject, names, - citations, commemorations), rendered five ways: csv (RFC 4180, - one header for the whole run), json, sexp, xml (schema/colitur- - v1.xsd) and ics (RFC 5545). --from/--to give a civil-year range, - inclusive. --dtstamp fixes the ics DTSTAMP so two runs over the - same data are byte-identical -- the engine reads no clock. - -rite: - --rite ef|of selects which rite module a command computes against; - default ef (so every invocation written before this flag - existed is unaffected). `of` is the 1970 Missale Romanum - (editio typica tertia, 2002). Accepted on `day`, `readings`, - `rubrics`, `temporal`, `emit`, `table`, `render` and `publish`; - refused, not silently ignored, on `check`/`convert` (operate - on an overlay FILE, not a computed year), `new-overlay` - (prints a static starter, no calendar computation), and - `lang`/`config` (answer naming/config questions orthogonal to - any rite). `easter` is refused too, but PROVEN rite-invariant - rather than merely unbuilt for `of`: EF and OF reckon Easter - on the identical Gregorian computus, so no second value could - ever change the six dates it prints. - - `emit --format csv --rite of` and `publish --rite of` widen - their CSV output with a 17th column, "second", between - "first" and "gospel" -- present, and usually empty, because a - Sunday or solemnity genuinely carries a Second reading (OLM - 1981 Praenotanda n. 66.1) and a feria/feast/memorial does not - (n. 69.1); EF's own 16-column CSV header is unaffected, byte - for byte, because EF's `citations` never contains one. json/ - xml/sexp/ics need no such widening -- each already had a place - for a variable-length reading list. - - A single `--out` directory can hold both rites' own `publish` - trees side by side ("ef/", "of/"), but not safely combined - with --prune on either: publish's own manifest and index.html - describe the WHOLE tree, not one rite's slice of it, so a - later run for the other rite would not know the first run's - files exist and --prune would delete them as stale. Publish a - single rite per --out, or omit --prune when deliberately - layering both. - -overlays: - --overlay FILE (repeatable, ordered; -o) applies a user calendar ON TOP of - the shipped universal one, never instead of it, so local feasts - add to it rather than replacing it. Later files win over earlier - ones, and over the universal calendar, when they name the same - slug. Accepted on `day`, `readings`, `rubrics`, `emit`, `table`, - `render` and `publish` -- `easter` and `temporal` read no - sanctoral data, so the flag is refused there rather than - silently ignored. - - An overlay is applied, NOT validated: colitur's test layers assert - things about the shipped calendar and cannot vouch for a file you - supply. A directive naming a slug that does not exist warns on - stderr and the run continues; a file that fails to load is fatal. - - To write one: - - colitur new-overlay > my-parish.sexp # a commented starter - $EDITOR my-parish.sexp - colitur check my-parish.sexp # parses? every directive hit? - colitur day 2026 --overlay my-parish.sexp - - `check` reports what each directive targets and exits 2 if a file - will not load or a directive matched nothing, so it fits a - Makefile or a pre-commit hook. It does not check a calendar - against the rubrics -- nothing here can. - - A flatter INI form exists for simple calendars, converted with - `colitur convert`, which verifies its own output before emitting - it. See colitur-overlay(5) for both forms. + colitur easter <year> Easter and the feasts anchored to it + colitur temporal <year> the temporal cycle, one line per day + colitur day <year> the resolved day, one line per day + colitur readings <year> the Mass reading citations + colitur rubrics <year> the Mass said, Creed, Gloria, preface + colitur emit --format FMT --from Y --to Y csv|json|sexp|xml|ics + colitur table|render --template FILE --year Y render through a template + colitur publish --from Y --to Y --out DIR write a static tree + colitur check FILE ... load an overlay and say what it does + colitur convert FILE.ini INI overlay -> S-expression, on stdout + colitur new-overlay print a starter overlay to stdout + colitur lang --list|--dump CODE|--check FILE + colitur config --show every setting and where it came from - In an added celebration, `citations` and `layer` may be omitted: - they default to empty and to the overlay's own id. Dates may be - (Fixed (month M) (day D)), (Easter_offset N) signed, or - (Nth_weekday (month M) (nth N) (weekday W)) with N negative to - count from the end of the month. +common flags: + --rite ef|of which form (default ef) + --year Y the year, as an alternative to the positional one + --month N print only that month; --date YYYY-MM-DD, --today + --pretty boxes for a person rather than rows for awk + --lang CODE|FILE name things in this language; --raw for bare slugs + --overlay FILE apply a local calendar (repeatable, ordered) + -h, --help this help -V, --version the version -year: - `day`, `readings`, `rubrics`, `table` and `render` each take a single - civil year, sayable two ways -- positionally (`colitur day - 2026`) or as --year (`colitur day --year 2026`) -- additively: - neither form was removed when the other was added, so every - invocation that worked before still works unchanged. Naming - both is fine as long as they agree (`colitur day 2026 --year - 2026`); naming both with DIFFERENT years is a hard usage error - rather than one silently winning. +<year> is 1583..9999 and means 1 January to 31 December, not a liturgical +year. emit and publish take --from/--to instead. A flag a command cannot +honour is refused, never silently ignored. - `emit`/`publish` deliberately do NOT gain a --year: they take - --from Y --to Y instead (see the usage block above), and stay - that way even for a single-year run (`--from 2026 --to 2026`) - -- a third spelling meaning exactly the same thing as the two - above would add parsing surface, and a range command's own - natural single-year form is already `--from Y --to Y`, not a - new flag. +exit status: 0 success; 2 bad usage, year out of range, or unreadable data. -naming: - --lang CODE|FILE applies to `day`, `readings`, `rubrics`, `emit`, - `table`, `render` and `publish`. A CODE (e.g. `la`, `en`) is - looked up as - <lang-dir>/CODE.ini; a value containing '/' or ending ".ini" is - read as a literal path instead. Default `la`, overridable by a - config file (see below). An unknown language is a hard ERROR - naming what is available (try `colitur lang --list`), never a - silent fallback to Latin -- a booklet quietly printed in the - wrong language is worse than one that refuses to print. A - language file may declare `fallback = CODE` in its [meta] - section, so a partial translation shows its fallback language - for the keys it does not itself carry, rather than bare slugs. - - --raw restores every command's pre-naming output: `name` (and every - other localised field `emit`'s schema carries) equals the bare - machine slug, exactly as if no language had ever been resolved. - This is not a special case threaded through the naming code -- - it is `colitur lang`'s own identity table, under which every - lookup echoes its key back unchanged. - - --sigla-style CODE|FILE, --sigla-book full|abbr, --sigla-tradition NAME - settings for how a Mass reading CITATION is written -- WHICH - punctuation/abbreviation style, WHICH book form, and WHICH - numbering tradition. Each has a config-key counterpart - (`sigla_style`/`sigla_book`/`sigla_tradition`) resolved with - the identical flag > config > default precedence as --lang, and - each is reported by `colitur config --show` with its source. - Defaults: `sigla_style` the resolved language code (so a - booklet's citations follow its own --lang unless told - otherwise), `sigla_book` `abbr`, `sigla_tradition` `vulgate`. - An unrecognised `--sigla-book` is a hard ERROR (want `full` or - `abbr`), the same discipline as an unknown `--lang`; an - unrecognised `--sigla-tradition` is not -- it degrades to the - Vulgate with a stderr warning, because asking for a renumbering - is optional the way asking for a language is not. `--raw` - bypasses all of it and emits each citation exactly as stored, - byte for byte, so it stays usable for diffing and does not - depend on the citation parser being correct. - - colitur lang --list which language files this build can find, and - each one's own declared fallback, if any. - colitur lang --dump CODE the named language's full key set, in INI - form, on stdout -- a starting point for a new - translation, or a way to diff two versions of - one. - colitur lang --check FILE what a language file is MISSING (a real slug - with no entry) and, separately, any entry - naming a slug that does not exist at all (a - typo, silently dead otherwise) -- exits 1 if - anything is unknown, so it fits a Makefile or - a pre-commit hook. - colitur config --show every effective setting -- lang, overlay, - template, format, sigla_style, sigla_book, - sigla_tradition -- its resolved value, and - where it came from: `flag`, `config` or - `default`. See colitur-config(5) for the - config file's location and full precedence. - -rendering: - --template FILE (required on `table`/`render`) is a logic-less Mustache- - family template: {{placeholder}}, {{#section}}...{{/section}}, - {{^inverted}}...{{/inverted}}, {{!comment}} -- nothing else. It is - DATA, never a program: no partials, no lambdas, no expression - evaluation, no filesystem or process access, and no "raw" or - triple-brace form that could opt out of escaping. The value it - renders against is the same schema `emit` uses (season, week, - slug, rank, colour, subject, names, citations, commemorations), - reshaped into a booklet (`days`) and a month grid (`weeks`, with - padding cells for the leading/trailing blanks); see - colitur-templates(5) for the full field list, the syntax, and - the one remaining scope hazard (`num` -- both a month and a week - carry it, and only the innermost one is ever meant). `name` is a - plain resolved string, not a lang-keyed object, so it carries no - equivalent hazard of its own. - - --flavour X selects how interpolated VALUES are escaped (never the - template's own literal markup, which is the author's). One of: - - latex typst groff html xml ics none - - Inferred from --template's extension when --flavour is omitted: - - .tex -> latex - .typ -> typst - .ms .mom .me -> groff - .html .htm -> html - .xml -> xml - .ics -> ics - .md .adoc .txt -> none (no metacharacters are escaped; - Markdown/AsciiDoc/plain text have no fixed - metacharacter set, so escaping them here - would produce worse output than leaving - them alone) - - An extension colitur does not recognise is a hard ERROR naming - the seven flavours above, never a silent fallback to `none`: - guessing wrong produces output that looks fine until the - metacharacters it silently failed to escape show up. - - `table` and `render` are the SAME operation under two names. The design - this project followed originally sketched `compute | render` as a - Unix pipe, with `render` reading a serialised view back from - stdin. That is deliberately not built: honouring the pipe would - need a JSON *parser*, purely so this program could re-read a view - it had just serialised itself -- a second hand-rolled component, - and a second place for the published contract to drift, for no - benefit over calling the view builder directly in the same - process. There is therefore no stdin-fed `render`; `colitur emit - --format json | jq` still composes for real pipe use, because - that JSON is the OUTPUT, never something colitur itself parses - back in. - -publish: - --out DIR (required) writes the static tree that IS this program's API: - any web server or git repo can serve it as-is, and nothing runs - at request time. - - <rite>/<year>.{json,csv,xml,ics} one civil year, all days - <rite>/<year>/<mm>/<dd>.json one file per day - schema/day-v1.json the published JSON contract - index.html a generated index page - .colitur-manifest every path this run wrote - - <rite> is "ef" or "of", selected by --rite exactly as on every - other command (default ef) -- see "rite" above for what a - single --out directory holding both rites' own trees needs. - - Deterministic: publishing the same --from/--to range twice - produces a byte-identical tree (--dtstamp behaves exactly as on - `emit`). That is what makes publishing into a git repo safe -- - `git status` shows only real change, and you review an actual - diff before pushing. - - Non-destructive: publish writes only files it owns, and records - every one in .colitur-manifest. A file you put in the output - directory yourself is never in that manifest, so it is never - touched, whether or not --prune is given. --prune additionally - removes manifest entries from a PREVIOUS run that this run did - not rewrite (e.g. an earlier year's per-day files, when you - publish a different range into the same directory) -- never - anything the manifest does not name. - -environment: - COLITUR_DATA_DIR - Read the calendar data from this directory instead of the - installed (<prefix>/share/colitur/ef) or build-tree location. - If it is set and holds no sanctoral.sexp, colitur exits 2 rather - than silently falling back to a different copy of the data. +Reading references only (e.g. "Jn 3:16"); never scripture text. -exit status: - 0 success - 2 bad usage, year out of range, or the calendar data could not be read +Full documentation -- every output format, the overlay and template file +formats, citation naming and numbering, the sources this computes against +and its stated limits: -Reading references only (e.g. "Jn 3:16"); never scripture text. -See colitur(1) for the full description and the sources it computes against, -colitur-overlay(5) for the overlay file format in full, -colitur-templates(5) for the template format in full -- the syntax, the -remaining scope hazard, the seven flavours' escaping, and the full view-model -field reference -- and colitur-config(5) for the config file's location and -precedence in full.|} + man colitur the program, in full + man 5 colitur-overlay the overlay format + man 5 colitur-templates the template format + man 5 colitur-config the config file|} let print_help () = print_endline help_text; diff --git a/colitur.opam b/colitur.opam index 4460ac4..69f4016 100644 --- a/colitur.opam +++ b/colitur.opam @@ -1,6 +1,6 @@ # This file is generated by dune, edit dune-project instead opam-version: "2.0" -version: "1.2.0" +version: "1.2.1" synopsis: "Deterministic liturgical calendar engine (computus liturgicus)" depends: [ "ocaml" diff --git a/data/of/expected-divergences-litcal.sexp b/data/of/expected-divergences-litcal.sexp index 633d2e0..a045415 100644 --- a/data/of/expected-divergences-litcal.sexp +++ b/data/of/expected-divergences-litcal.sexp @@ -139,6 +139,34 @@ ; RG 112(a) does on the EF side for a narrower case. NOT fixed here (a ; kernel-level tie-break policy is out of this task's own scope regardless ; of verdict). +; +; UPDATE 2026-08-28: a CANDIDATE citation has since been found, and it +; points AGAINST colitur's current answer. Tabula I.3 does not name a bare +; category; it enumerates one in a definite order -- "Sollemnitates DOMINI, +; beatae Mariae Virginis, et SANCTORUM in Calendario generali inscriptae". +; The Sacred Heart is a Solemnity of the Lord; the Nativity of St John the +; Baptist is a Solemnity of a Saint. Read as ordered, that entry puts the +; Lord first, litcal's answer is right and colitur's alphabetical tie-break +; is reaching a conclusion the text already settles. colitur's own EF side +; models exactly this shape already -- RG 91 entry 14's "primum mobilia, +; deinde fixa" is a within-entry order, and Precedence_ef.band expresses it +; by scaling the whole table x10 so a half-row sits between its neighbours. +; Precedence_of.band is scaled x10 too, so I.3 could carry 30/31/32 with no +; structural change. +; +; The counter-argument, which is why this stays OPEN rather than becoming a +; fix: Tabula entry 4 marks its own internal order with explicit a) b) c) d) +; lettering, so the drafters had a way to signal ordering and used it. Entry +; 3 is a plain "A, B, et C" list. Entry 2's own list ("Nativitas Domini, +; Epiphania, Ascensio et Pentecostes...") cannot be an ordering at all, +; since those days can never collide with each other -- which is evidence +; that a list inside an entry need not imply precedence. +; +; What would settle it: any authoritative source treating 24 June in a +; Sacred-Heart-collision year -- an ordo, a Notitiae reply, or a +; conference decree. The collision is rare (Easter must be late enough to +; put Easter+68 on 24 June) and none of the project's current witnesses +; covers such a year. Verdict stays open; behaviour unchanged. ((id L6) (citation "precedence.ml's compare_by (Slug.compare tie-break); no Tabula/Normae text found ranking Sacred Heart above/below the Nativity of John the Baptist at equal band") (verdict open) diff --git a/data/of/lectionary.sexp b/data/of/lectionary.sexp index 693b768..bf3b5eb 100644 --- a/data/of/lectionary.sexp +++ b/data/of/lectionary.sexp @@ -63,7 +63,7 @@ ; COVERAGE (5) below for the measured count. ; ; Source: ../lectio/internal/caldata/of-lectionary.ini -; SHA-256: f0b527aef58b93e6ccdfbeafe52b340ed2a9d7d72805bf5083e3da04b080123c +; SHA-256: 7b6c6a26abf222312c3000aeaae131887bbb1e2af3863c68a3e5ad005cd07ba9 ; 988 ini sections (988 expected) -> 438 resolved (base, cycle-shape) pairs -> 435 colitur slugs mapped, 3 ini bases genuinely excluded (structurally ; unreachable -- see [excluded_bases]), 0 ini bases genuinely unmapped ; (no pattern, no override, no matching sanctoral slug) -> 771 emitted @@ -215,7 +215,7 @@ ((part Gospel) (reference "Luke 5:17-26")))) (of-advent-2-saturday (((part First) (reference "Sirach 48:1-4,9-11")) - ((part Gospel) (reference "Matthew 17:9a,10-135")))) + ((part Gospel) (reference "Matthew 17:9a,10-13")))) (of-advent-2-thursday (((part First) (reference "Isaiah 41:13-20")) ((part Gospel) (reference "Matthew 11:11-15")))) @@ -929,7 +929,7 @@ ((part Gospel) (reference "Mark 1:14-20")))) (of-ordinary-time-1-monday-ii (((part First) (reference "1 Samuel 1:1-8")) - ((part Gospel) (reference "Mark 1:14-22")))) + ((part Gospel) (reference "Mark 1:14-20")))) (of-ordinary-time-1-saturday-i (((part First) (reference "Hebrews 4:12-16")) ((part Gospel) (reference "Mark 2:13-17")))) @@ -946,13 +946,13 @@ (((part First) (reference "Hebrews 2:5-12")) ((part Gospel) (reference "Mark 1:21-28")))) (of-ordinary-time-1-tuesday-ii - (((part First) (reference "1 Samuel 1:9-22")) + (((part First) (reference "1 Samuel 1:9-20")) ((part Gospel) (reference "Mark 1:21-28")))) (of-ordinary-time-1-wednesday-i (((part First) (reference "Hebrews 2:14-18")) ((part Gospel) (reference "Mark 1:29-39")))) (of-ordinary-time-1-wednesday-ii - (((part First) (reference "1 Samuel 3:1-10,19-22")) + (((part First) (reference "1 Samuel 3:1-10,19-20")) ((part Gospel) (reference "Mark 1:29-39")))) (of-ordinary-time-10-friday-i (((part First) (reference "2 Corinthians 4:7-15")) @@ -1061,7 +1061,7 @@ ((part Gospel) (reference "Matthew 7:15-20")))) (of-ordinary-time-12-wednesday-ii (((part First) (reference "2 Kings 22:8-13;23:1-3")) - ((part Gospel) (reference "Matthew 7:15-22")))) + ((part Gospel) (reference "Matthew 7:15-20")))) (of-ordinary-time-13-friday-i (((part First) (reference "Genesis 23:1-4,19;24:1-8,62-67")) ((part Gospel) (reference "Matthew 9:9-13")))) @@ -1259,7 +1259,7 @@ ((part Gospel) (reference "Matthew 17:14-20")))) (of-ordinary-time-18-saturday-ii (((part First) (reference "Habakkuk 1:12-2:4")) - ((part Gospel) (reference "Matthew 17:14-22")))) + ((part Gospel) (reference "Matthew 17:14-20")))) (of-ordinary-time-18-thursday-i (((part First) (reference "Numbers 20:1-13")) ((part Gospel) (reference "Matthew 16:13-23")))) @@ -1313,7 +1313,7 @@ ((part Gospel) (reference "Matthew 18:15-20")))) (of-ordinary-time-19-wednesday-ii (((part First) (reference "Ezekiel 9:1-7;10:18-22")) - ((part Gospel) (reference "Matthew 18:15-22")))) + ((part Gospel) (reference "Matthew 18:15-20")))) (of-ordinary-time-2-friday-i (((part First) (reference "Hebrews 8:6-13")) ((part Gospel) (reference "Mark 3:13-19")))) @@ -1745,7 +1745,7 @@ ((part Gospel) (reference "Mark 4:1-20")))) (of-ordinary-time-3-wednesday-ii (((part First) (reference "2 Samuel 7:4-17")) - ((part Gospel) (reference "Mark 4:1-22")))) + ((part Gospel) (reference "Mark 4:1-20")))) (of-ordinary-time-30-friday-i (((part First) (reference "Romans 9:1-5")) ((part Gospel) (reference "Luke 14:1-6")))) @@ -1937,7 +1937,7 @@ ((part Gospel) (reference "Mark 5:1-20")))) (of-ordinary-time-4-monday-ii (((part First) (reference "2 Samuel 15:13-14,30;16:5-13")) - ((part Gospel) (reference "Mark 5:1-22")))) + ((part Gospel) (reference "Mark 5:1-20")))) (of-ordinary-time-4-saturday-i (((part First) (reference "Hebrews 13:15-17,20-21")) ((part Gospel) (reference "Mark 6:30-34")))) @@ -2050,7 +2050,7 @@ (((part First) (reference "Sirach 17:1-15")) ((part Gospel) (reference "Mark 10:13-16")))) (of-ordinary-time-7-saturday-ii - (((part First) (reference "James 5:13-22")) + (((part First) (reference "James 5:13-20")) ((part Gospel) (reference "Mark 10:13-16")))) (of-ordinary-time-7-thursday-i (((part First) (reference "Sirach 5:1-8")) diff --git a/dune-project b/dune-project index 9b47223..8287bf1 100644 --- a/dune-project +++ b/dune-project @@ -1,6 +1,6 @@ (lang dune 3.0) (name colitur) -(version 1.2.0) +(version 1.2.1) (generate_opam_files true) (cram enable) diff --git a/man/colitur.1 b/man/colitur.1 index 4d639f8..8a00a88 100644 --- a/man/colitur.1 +++ b/man/colitur.1 @@ -81,6 +81,10 @@ colitur \- deterministic liturgical calendar and lectionary engine (Roman rite, .B colitur .BR \-h | \-\-help .SH DESCRIPTION +.\" --help was trimmed to one screen (2026-08-28); this page is now the only +.\" place the formats, file syntaxes and hazards are written down, which is +.\" what that help text tells the reader. + .B colitur computes the liturgical calendar of the 1962 Roman Missal \(em the Extraordinary Form \(em and the Mass reading citations for every day, entirely @@ -578,10 +578,10 @@ error, exit 2, leaving stdout clean for whatever the caller was really trying to capture. $ colitur --help | head -1 - colitur -- deterministic liturgical calendar engine (Roman rite, 1962) + colitur -- deterministic liturgical calendar engine (Roman rite, EF 1962 and OF) $ colitur -h | head -1 - colitur -- deterministic liturgical calendar engine (Roman rite, 1962) + colitur -- deterministic liturgical calendar engine (Roman rite, EF 1962 and OF) $ colitur --help > /dev/null $ colitur -h > /dev/null @@ -601,21 +601,26 @@ is the check that catches help text drifting away from the dispatch: $ for c in easter temporal day readings rubrics; do colitur $c 2026 > /dev/null || echo "$c FAILED"; done `rubrics`' own field count drifted once already (Task 5 landed a fourth -column, creed; --help's "output formats" section and its worked example +column, creed; the documented "output formats" section and its worked example still showed three, and stayed wrong for a whole review round because -nothing here pinned the help BODY, only command dispatch above). This -compares --help's own worked example against what `rubrics` actually +nothing here pinned the documentation BODY, only command dispatch above). This +compares the DOCUMENTED worked example against what `rubrics` actually prints for the identical day, so the two cannot silently drift apart -again in either direction: +again in either direction. - $ colitur --help | grep '^ rubrics date' - rubrics date, formulary slug, source, creed, gloria, preface [name] -- TAB-separated +The comparison reads the man page, not --help: --help was trimmed to one +screen and now points at colitur(1) for every format, so this is where the +worked example lives. The man page is named in test/dune's own cram deps for +exactly this reason -- a cram sandbox holds only what that stanza lists. - $ colitur --help | sed -n '/^ rubrics date/{n;p}' - 2026-01-01[TAB]ef-circumcision[TAB]own[TAB]true[TAB]true[TAB]nativity[TAB]In Octava Nativitatis Domini + $ grep -c 'formulary\\-slug .TAB. source .TAB. creed' ../man/colitur.1 + 1 + + $ sed 's/\\//g' ../man/colitur.1 | grep -o '2026-01-01 \[TAB\] ef-circumcision.*' | head -1 + 2026-01-01 [TAB] ef-circumcision [TAB] own [TAB] true [TAB] true [TAB] nativity [TAB] In Octava Nativitatis Domini - $ colitur rubrics 2026 | grep '^2026-01-01' | sed $'s/\t/[TAB]/g' - 2026-01-01[TAB]ef-circumcision[TAB]own[TAB]true[TAB]true[TAB]nativity + $ colitur rubrics 2026 | grep '^2026-01-01' | sed $'s/\t/ [TAB] /g' + 2026-01-01 [TAB] ef-circumcision [TAB] own [TAB] true [TAB] true [TAB] nativity --version prints the version alone, to standard output, exit 0. Deliberately not embedded in the help text above: this pin would then have to be edited @@ -105,4 +105,11 @@ ../schema/day-v1.json ; Same trap, same fix: Task 8's --sigla-tradition flag makes the CLI read ; this file, and a cram sandbox holds only what this stanza names. - ../lang/traditions.ini)) + ../lang/traditions.ini + ; The man page is a DEP, not a runtime file the binary reads: cli.t's own + ; rubrics anti-drift check compares the documented worked example against + ; what `rubrics` actually prints, and that documentation now lives here + ; rather than in --help (trimmed to one screen). The check exists because + ; this exact field list drifted once and stayed wrong for a whole review + ; round; moving the prose without moving the check would quietly retire it. + ../man/colitur.1)) diff --git a/test/test_lectionary_of.ml b/test/test_lectionary_of.ml index dc02176..cbffdd2 100644 --- a/test/test_lectionary_of.ml +++ b/test/test_lectionary_of.ml @@ -201,7 +201,7 @@ let test_sha256_pinned () = and test_citation_coverage_of.ml), and the header's "Epistle" wording is fixed to "First". Re-derived the same way, same discipline. *) Alcotest.(check string) "data/of/lectionary.sexp SHA-256" - "788ddc6d2d783286b6e64882a5372ae213cd5462c887e68aa2bea22388cdb4f7" (sha256_of_file lectionary_path) + "aaa57bd7229bbd1fb3372fb9467251419885af6f02dbd19d328a6c410030868d" (sha256_of_file lectionary_path) (* lectio's OWN of-lectionary.ini SHA-256, pinned inside data/of/ lectionary.sexp's own provenance header (tools/bootstrap_lectionary_of @@ -216,7 +216,7 @@ let test_source_sha256_pinned_in_header () = let n = in_channel_length ic in let content = really_input_string ic n in close_in ic; - let needle = "SHA-256: f0b527aef58b93e6ccdfbeafe52b340ed2a9d7d72805bf5083e3da04b080123c" in + let needle = "SHA-256: 7b6c6a26abf222312c3000aeaae131887bbb1e2af3863c68a3e5ad005cd07ba9" in let contains haystack needle = let hl = String.length haystack and nl = String.length needle in let rec go i = i + nl <= hl && (String.sub haystack i nl = needle || go (i + 1)) in @@ -488,6 +488,77 @@ let test_christmas_date_keyed_no_drift () = check_year 2024; check_year 2025 +(* ------------------------------------------------------------------ *) +(* In ORDINARY TIME the OF weekday Gospel does not depend on which year of + the two-year cycle is running: OLM n. 69 point 4 alternates the FIRST + reading between Year I and Year II and leaves the Gospel a single + one-year series. So for every [of-ordinary-time-<n>-<day>-i] there is an + [...-ii] whose Gospel reference is the SAME STRING, and a pair that + disagrees is a data defect rather than a liturgical fact. + + This is deliberately scoped to Ordinary Time. The same sweep over the + whole file reports 17 further disagreeing pairs in [of-advent-3/4-*] and + [of-christmas-*] -- all of them CORRECT: those weekdays are keyed to the + calendar DATE (17-24 December, and the Christmas octave, OLM n. 69 + point 3, the rule [date_keyed_slug] above implements), so their -i/-ii + entries describe two different dates rather than two years of one cycle. + An unscoped version of this test would report seventeen false positives + and would, correctly, be switched off by whoever next read it. + + CLOSED (2026-08-28, same day it was opened). Six pairs violated this when + the test was written, every one the same transcription fault: a Year II + Gospel ending at verse 22 where the OLM ends it at 20. All six are now + corrected upstream in lectio's own internal/caldata/of-lectionary.ini and + this file regenerated from it, so the expected violation set is EMPTY and + the test asserts the invariant outright rather than pinning exceptions to + it. Four further corruptions the same exercise found -- three First + readings and of-advent-2-saturday's "Matthew 17:9a,10-135", a verse + Matthew 17 does not have -- are fixed in the same upstream commit but are + invisible to THIS check by construction: no I/II symmetry constrains a + First reading, and Advent has no year cycle. They were caught by comparing + against litcal instead, and the two SHA-256 pins above are what stop the + data regressing underneath either. + + Corrections confirmed verbatim in the Latin OLM 1981: "Mc 1, 14-20", + "Mc 4, 1-20", "Mc 5, 1-20", "Mt 7, 15-20", "Mt 17, 14-20", + "Mt 18, 15-20". *) +let known_year_ii_gospel_defects = [] + +let test_ordinary_time_gospel_is_year_independent () = + let entries = Lectionary.entries (real_lectionary ()) in + let gospel_of cits = + match List.find_opt (fun c -> c.Colitur_kernel.Citation.part = Colitur_kernel.Citation.Gospel) cits with + | Some c -> Some c.Colitur_kernel.Citation.reference + | None -> None + in + let by_slug = List.map (fun (s, c) -> (Slug.to_string s, gospel_of c)) entries in + let find s = match List.assoc_opt s by_slug with Some g -> g | None -> None in + let has_prefix p s = String.length s >= String.length p && String.sub s 0 (String.length p) = p in + let has_suffix suf s = + String.length s >= String.length suf && String.sub s (String.length s - String.length suf) (String.length suf) = suf + in + let ordinary_time_stems = + List.filter_map + (fun (s, _) -> + if has_prefix "of-ordinary-time-" s && has_suffix "-i" s && not (has_suffix "-ii" s) then + Some (String.sub s 0 (String.length s - 2)) + else None) + by_slug + in + let stems = List.sort_uniq compare ordinary_time_stems in + let violations = + List.filter + (fun stem -> + match (find (stem ^ "-i"), find (stem ^ "-ii")) with + | Some a, Some b -> a <> b + | _ -> false) + stems + in + Alcotest.(check bool) "the sweep actually found Ordinary Time pairs to check" true (List.length stems > 100); + Alcotest.(check (list string)) + "Ordinary Time: Year I and Year II share one Gospel, with no exceptions" + known_year_ii_gospel_defects (List.sort compare violations) + let suite = [ Alcotest.test_case "Sunday cycle table (OLM n.66, straddles Advent I)" `Quick test_sunday_cycle_table; Alcotest.test_case "weekday cycle table (OLM n.69.4, straddles Advent I)" `Quick test_weekday_cycle_table; @@ -504,5 +575,8 @@ let suite = Alcotest.test_case "O-Antiphon date-keyed reading does not drift (17 December)" `Quick test_o_antiphon_no_drift; Alcotest.test_case "Christmas-season date-keyed reading does not drift (2 January)" `Quick - test_christmas_date_keyed_no_drift + test_christmas_date_keyed_no_drift; + Alcotest.test_case + "Ordinary Time Gospel is year-independent (OLM n. 69.4)" `Quick + test_ordinary_time_gospel_is_year_independent ] |
