aboutsummaryrefslogtreecommitdiff
path: root/tools
Commit message (Collapse)AuthorAgeFilesLines
* fix(of-lectionary): stop serving drifting readings for O-Antiphon and ↵Lukasz Kasprzak2026-08-261-53/+175
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Christmas-season dates The bootstrap excluded 19 lectio bases as "date-keyed duplicates", but 17 of them were not duplicates at all: the 8 O-Antiphon days (17-24 December) and 8 further Christmas-season dates (29-31 December, 2-5 and 7 January) each carry unique per-date content found nowhere else among the emitted entries, verified against lectio's own ini directly. 17 December is Gen 49:2,8-10 / Matthew 1:1-17, but colitur was falling through to that year's unrelated weekday-keyed "advent-3-mon" content instead. OLM n. 69.3 explains why: those ferias are fixed by civil date, unlike every other Advent/Christmastide/Paschaltide feria, which is merely non-alternating within a weekday slot. Temporal_of's own ferial slugs for these dates are weekday-keyed, so looking one of those up served whichever OTHER date happened to share that year's weekday alignment -- a citation that silently drifted year to year on ~16 real civil dates (6 January stays excluded: Temporal_of fixes Epiphany there unconditionally, so that date can never reach the ferial path at all; two more, easter-6-thu and advent-4-sat, stay excluded for their own, different structural reasons). Fixed with a date-keyed lookup route (Lectionary_of.date_keyed_slug), tried before the weekday-keyed one in readings' own step 3. Temporal_of's slugs are unchanged -- only which lectionary key resolves a day's citations. tools/bootstrap_lectionary_of.ml now maps the 17 bases to synthetic date-keyed slugs instead of excluding them, and its own reachability sweep calls date_keyed_slug directly so the two can never drift apart. Re-emitted data/of/lectionary.sexp (754 -> 770 entries) and corrected its provenance header, which previously described these dates as excluded duplicates. Also states, for the first time, a Minor finding from the same review: 190 of 222 shipped sanctoral slugs have no dedicated lectionary entry and correctly fall through to the ferial per OLM norms -- expected behaviour, not a gap, but never said in the coverage report before. New tests pin 17 December (Gen 49:2,8-10 / Matthew 1:1-17) and 2 January (1 John 2:22-28 / John 1:19-28) in two years with different weekday alignments, proving neither reading drifts, plus direct boundary coverage of date_keyed_slug itself (both windows, the 6 January exclusion, and the Sunday guard).
* feat(of): Lectionary_of -- reading-cycle arithmetic and the lectionary chainLukasz Kasprzak2026-08-262-0/+842
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Task 4 of Phases 3-5: the last Rite.t field OF still needed, `readings`. The cycle rules are CODE, cited to OLM (Ordo Lectionum Missae) 1981 Praenotanda -- verified against the real page image (docs/research/of/olm-1981.pdf pp.32-33/"XXXII-XXXIII"), not the document's own unreliable OCR text layer, per the design spec's own caveat. n. 66 (with its footnote 102's worked example, 1980=C/1981=A/ 1982=B/1983=C) gives the three-year Sunday cycle; n. 69 point 4 gives the two-year weekday cycle, Year I in odd label years, Year II in even. Both derive the "label year" the same way: one more than the civil year Advent I opened in, per n. 66's footnote 102 itself ("nempe a prima hebdomada Adventus, quae cadit in anno civili praecedente") -- so the cycle turns at Advent I, not 1 January, exactly where the task brief said to get it right. The design spec's own citation for the weekday rule, "Praenotanda n. 649", does not survive a direct check against the primary source (search finds "649" only in unrelated index/page-number entries); corrected here to n. 69, the paragraph the rule's own Latin text actually sits under, confirmed by the page image's own printed margin number. The data is bootstrapped from lectio's of-lectionary.ini (988 keys, niedziela.pl, harvest 2020-2025) by tools/bootstrap_lectionary_of.ml -- OCaml, not the brief's own suggested .py: tools/bootstrap_lectionary .ml already established this exact shape for EF, including a real safety net ([assert_reachable], sweeping Rite_of.Temporal_of.temporal AND the real merged sanctoral layer directly) that a second-language reimplementation would either duplicate or lose. Handles two real data anomalies found while building it: one malformed ini line (a Polish Septuagint-numbering annotation for Sirach 3, "Syr 3,2-6.12-14" against OLM/CEI/USA's shared Nova Vulgata, matching the design spec's own sec5 finding); and two Sunday-cycle-labelled bases (Annunciation, Immaculate Conception) where a scraping-year artifact left one of three cycle letters carrying a different day's Mass entirely -- resolved by a general, mechanical 2-of-3 majority rule, not a per-saint hand edit, and logged. Coverage measured in both directions, per the brief's own Step 1: - 1 of 365 days in civil year 2026 (25 December, the Christmas DAY Mass) has no Temporal_of-slug match anywhere in lectio's 988 keys -- only the Vigil is present. Named in the data file's own header. - 0 of 438 distinct lectio bases map to no colitur slug, after 19 are deliberately excluded (late-Advent/Christmas-season DATE-keyed duplicates lectio also carries in a WEEKDAY-keyed form colitur's own slugs actually match -- Lectionary.t has no date-keyed lookup at all, so the date-keyed family is transcription-inert, not silently dropped: named explicitly, not a residual). Lectionary_of.readings is three steps (no Commons indirection, unlike EF -- out of this task's scope): the observed celebration's own embedded citations; else, for a real sanctoral winner, its own slug looked up directly (flat, then both cycle letters); else the day's own temporal slug, same lookup. No preceding-Sunday fallback: OF's own two-year cycle assigns every Ordinary Time weekday its own reading by design, and no OF norm was found instructing otherwise. Tests (test/test_lectionary_of.ml, 12 cases): a cycle-arithmetic table straddling Advent 1980/1981/1982/2025 inside single civil years, reproducing OLM n.66's own worked example directly; SHA-256 pins on both the emitted file and lectio's own source (embedded in the provenance header); a pinned, NAMED 2026 coverage result (364 resolve, exactly {25 December} does not -- never a bare count); two resolution- chain spot checks (a plain ferial, and a saint's day beating the ferial it would otherwise fall on); and a real subprocess test that a missing lectio source file fails the generator loudly, non-zero exit, no output written -- the exact bug Task 1's own generator shipped and needed a review round to fix. dune test: exit 0, 856 tests. make check (COLITUR_EXHAUSTIVE_SWEEP=1, full 1583-9999 sweep): exit 0, 862 tests, ~342s, both run unpiped in the foreground. git diff --stat v1.0.0..HEAD -- lib/rites/rite_ef/ lib/kernel/ data/ef/ still empty.
* fix(of): correct subject over-match, provenance truth, and stale hazard commentLukasz Kasprzak2026-08-251-29/+153
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | Four fixes from fix round 2 review: classify_subject's "maria"/"b.m.v" substring test over-matched 12 saints who merely carry "Maria" in their own name (Maximilian Mary Kolbe, John Mary Vianney, Mary Magdalene among them) as Bvm. Replaced with an explicit BVM_DATES table of the 13 genuinely Marian entries. Bvm 25 -> 13. The provenance header claimed lectio is "never a source", which omitted that slugs and English names both come from it. Corrected to state exactly what does (slugs, en names) and does not (every substantive field) come from lectio. A missing lectio file used to degrade silently to a different SHA-256 with no en names and no warning; it now fails loudly. lectio's own SHA-256 is now pinned alongside the Missal PDF's. Four more entries carried a second pdftotext artifact distinct from the letter-spacing already handled -- a combining diacritic rendered as a stray spacing character (Kim Taegŏn/Chŏng Ha-sang, Đũng Lạc, Makhlūf, Kęty). Hand-repaired the same way Peter Damian was, each corroborated by the identical corruption recurring in the Missal's own index. precedence_of.ml's PHASE 3 HAZARD comment about All Souls (2 November) was asserting a gap that no longer exists now that OF sanctoral data ships. Added test_all_souls_beats_an_ordinary_sunday, which resolves the shipped All Souls entry against a real per-annum Sunday (2025-11-02) through the actual Precedence.resolve pipeline, and updated the comment to record the resolution instead of demanding it.
* fix(of): add the Sacred Heart of Jesus and Immaculate Heart of MaryLukasz Kasprzak2026-08-251-51/+208
| | | | | | | | | | | | | | | | | | | | Both are printed in the 2002 calendar table, immediately after 30 June, as prose headings rather than (kalends, day) rows -- the shape parse_rows' generic movable-heading skip branch was built for, which is why the first pass silently dropped them. They belong in the transcription, not in Temporal_of: adding them completes the table this file transcribes rather than amending the temporal cycle. Produced as Date_spec.Easter_offset entries (68 and 69 respectively), the same variant already used for EF's Rogation Wednesday and needing no kernel change. Pentecost is Easter+49, so the Friday and Saturday after the Second Sunday after Pentecost are Easter+68/+69 -- verified against three real Easter dates (2026, 2027, 2035), not merely computed on paper. The extractor now recognises these two headings specifically among the seven "Dominica/Feria/Sabbato ... :" headings the table contains; a full audit of the other five confirms each is already covered by Temporal_of and correctly stays excluded here. 206 -> 208 entries.
* feat(of): transcribe the 2002 General Roman CalendarLukasz Kasprzak2026-08-251-0/+677
| | | | | | | | | | | | | | | | | Extracted from the Latin typical edition, which is the authority; lectio's roman-calendar.ini is a cross-check only and every divergence is adjudicated against the Missal in the provenance header. A blank grade column means Memoria ad libitum, per the calendar's own footnote, rather than a missing value. 206 entries; 1 January, 6 January and 25 December are deliberately excluded because Temporal_of.named already computes them, and shipping them here too would create a duplicate candidate for the same day. Colour is derived from IGMR n. 346, cited per class in the extractor; the lectio cross-check surfaces one rank divergence (22 July, Mary Magdalene -- lectio reflects the 2016 post-2002 elevation decree, out of scope here) and 17 colour divergences, all adjudicated in the provenance header.
* test(of): validate Phase 1's Ordinary Time resumption against litcalLukasz Kasprzak2026-08-251-0/+450
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Adds a sixth-lineage external validation layer for the OF rite module's Phase 1 temporal cycle, targeting the single highest-risk function that report named: the two-block Ordinary Time week arithmetic (Normae n. 44), which has no external witness anywhere in Phase 1's own suite and whose output selects the Mass formulary, not merely a label. Witness: litcal (Liturgical Calendar API), Apache-2.0, already archived and SHA-256-pinned by the sibling lectio project. AELF was considered and correctly excluded earlier (its CGU forbids redistribution) -- litcal is what it was replaced with. - tools/extract_litcal_ordo.py verifies all twelve archived calendar-YYYY-en.json files (2024-2035; the task brief said eleven, but twelve are actually pinned in lectio's manifest -- all twelve are used) against lectio's own manifest.tsv, then builds one fixture spanning the contiguous 4382-day range those twelve liturgical-year files cover. - test/fixtures/litcal-temporal-2024-2035.sexp: one row per day, season and (where witnessed) Ordinary Time week, with full provenance, licence, per-year Easter dates and the row-selection rules in its own header. - test/test_litcal_of.ml calls Temporal_of.temporal directly (no Rite.t) and compares 1:1 against the fixture: zero Ordinary Time week mismatches on all 1876 witnessed days (zero tolerance, no allow-list); the only season divergence class is the Sacred Triduum, which litcal tags with a season value colitur's vocabulary deliberately has none of -- one cited allow-list entry, data/of/expected-divergences-litcal.sexp, 36 rows, exact. - Mutation-tested: dropping the backward-from-34 adjustment reddens this layer (1511 of 1876 days) plus three pre-existing Phase 1 tests; the mutation was not committed. Does not touch lib/rites/rite_ef/, data/ef/, or lib/rites/rite_of/ -- this validates Phase 1, it does not change it. Full suite: 786 tests via dune test, 791 via make check, exit 0 both; lib/bin diff empty throughout.
* feat(ordo): wire the extraordinaryform.org preface column in as a third witnessLukasz Kasprzak2026-08-241-0/+336
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Extends the preface comparison (RG 482-499) with a third, independent publisher, extraordinaryform.org's three annual Ordo editions (2024-2025, 2025-2026, 2026-2027), previously declined for two reasons that are now resolved: - "Advent" is not a preface: RG 494(b) grants the Trinity preface on every Advent SUNDAY, RG 498 the residual Common on every Advent FERIA; this source's own "Advent" label covers both indiscriminately (confirmed live on all 11 non-impeded Advent Sundays in the corpus). Classified by the day's own civil weekday, not a static table. - The known Vigil-of-St-Lawrence gap is reconfirmed, with a nuance not present in the earlier audits: colitur's own RG33-third-trigger now omits the vigil in some years too, so the corpus's gap and colitur's answer coincide in 2025 (a full match, not a coincidence) while 2027 still shows the classic identity-wrong-but-preface-invisible shape. Re-derives the known menu-trap office filter (Requiem-skip, "and "-chain) against the live JSON and finds a second, general pattern: "Saturday of Our Lady" (RG78's BVM Saturday Office), whenever listed, is unconditionally the office -- checked exhaustively across all 38 instances in the corpus, zero counter-examples. A third, generic "absorption" rule was deliberately not built: it would resolve 2 of the 11 residual divergences but wrongly misfires on a structurally identical but semantically different shape (an optional Rogation-day Mass option). The JSON's own extraction carries a genuine classifier gap: parse_ordo.py's hardcoded PREFACES set omits "Christ the King", misfiling it into comms; recovered in the generator, not left blank. 11 of 1088 comparable rows diverge, resolving to 7 adjudicated root causes (E1-E7): two Commemoration_only Marian titles reduced to commemorations (Mt Carmel, Ransom), two genuine sanctoral data gaps cross-confirmed absent by the FIUV/LMS witnesses (Miraculous Medal, St John before the Latin Gate), two single-witnessed efdotorg outliers against FIUV/LMS agreement (St Michael's Dedication, All Saints on a Sunday), and one isolated source labelling slip. Mutation-tested: corrupting the RG495 BVM-Saturday clause reddens 9 tests across this new layer and both existing Ordo witnesses. Per-edition agreement: 358/363 (2024-2025), 358/363 (2025-2026), 361/362 (2026-2027).
* feat(ordo): wire the LMS preface column into the witness suiteLukasz Kasprzak2026-08-231-9/+189
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Extends tools/extract_lms_ordo.ml's row type with a praef field (the first "Pr of ..." / "Common Pr" match in each day's universal block, same first-in-reading-order discipline already used for Gl/Cr) and regenerates all three LMS fixtures. Also brings lms-ordo-2024-2025.sexp under the generalised, probe-verified provenance template the Witnesses task previously added only for the two newer editions -- it had kept Task 6's original hand-written header. Characterisation finding: this Ordo prints OPTION LISTS ("Pr of X or Pr of Y or Common Pr"), not a single value per day, and several named options (Martyrs, All Saints and Patron Saints, the Dedication of a Church, the Most Holy Sacrament, St John the Baptist, the Angels) are genuinely absent from RG 484-497's own fourteen named prefaces. Every instance found in the universal block pairs such an extra with a genuine RG-482 answer, so test/test_lms_ordo.ml compares by membership (classify_praef_options, a 15-entry prefix table split on the standalone token "or"), not string equality. Opposite-prediction probes (Nativity/Apostles/Requiem) are hard- verified inside the extractor itself before any fixture is trusted, the same discipline already used for the Creed column. Result: 396/396, 399/399, 398/398 comparable rows agree across the three editions (Good Friday excluded per edition, no Mass that day), zero allow-list entries needed. extraordinaryform.org's own three editions were characterised but deliberately not wired in this pass -- see .superpowers/sdd/2026-08-21-colitur-celebrant-rubrics-phase1/preface-witnesses-report.md for the full account (the field is genuinely per-day, contrary to a naive header-count read, but carries its own classification anomaly during Advent and reconfirms the corpus's known St Lawrence vigil defect, and building a full extractor for it is a separate, comparably large task not needed to decide the RG 497 question this same report settles). Claude-Session: https://claude.ai/code/session_017ZBxCCRM2ojnBupp3SBxV9
* fix(ef): ferias after the Ascension repeat its own Mass, not a Sunday'sLukasz Kasprzak2026-08-221-0/+70
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Ascension has no octave under the 1960 rubrics -- confirmed structurally: the Proprium de Tempore's own table of contents runs straight from "In Ascensione Domini" to "Dominica post Ascensionem" with no per-feria headings in between. Step 3 of the reading chain therefore fell through to Preceding_sunday on every unoccupied weekday in that stretch, walking each one back to the 5th or 6th Sunday after Easter's own citation. The Missal's own Proprium de Tempore carries an explicit marginal rubric immediately after the Ascension's Mass propers, before "Dominica post Ascensionem" begins: "Diebus a feria VI post Ascensionem usque ad feriam VI ante vigiliam Pentecostes inclusive, dicitur Missa de festo Ascensionis, cum Gloria et praefatione de Ascensione, sine Credo et Communicantes proprio" -- every feria without its own proper, from the Friday after the Ascension through the Friday before the Vigil of Pentecost, repeats the Ascension's own Mass. RG 490(b) corroborates independently: it assigns the Ascension preface "tamquam de Tempore" across that identical window, which only makes sense if the Mass actually repeated there is the Ascension's, not a numbered Sunday's (whose own Mass would carry no need for a preface override at all). Witnessed directly, not only inferred from the rubric: the LMS Ordo prints "Mass of the Ascension" literally on every affected day across all three of its fixture windows, in BOTH the tail of Ascension's own week and the following week -- ruling out the alternative reading (that the second week instead resumes "Dominica post Ascensionem"'s own Mass, which a first pass over the FIUV Ordo's own week-labelled titles could suggest at a glance; its own [praef] field for the same two dates independently reads "Ascensionis", resolving the apparent conflict). Implemented as data, not logic, matching this codebase's own established precedent for the identical RG 299 shape (the week after Trinity Sunday, data/ef/lectionary.sexp's [trinity_week_entries]): six new hand-authored entries, [ascension_week_entries] in tools/bootstrap_lectionary.ml, regenerated against a git-pinned copy of lectio's own source ini (SHA-256 verified unchanged) so the regeneration carries no unrelated drift. The window's two Saturdays are deliberately excluded -- RG 78's votive Office of Our Lady on Saturday always wins an otherwise-unoccupied Class4 Saturday first, before step 2 (this fix's own entry point) is ever reached, so an entry for either slug would be dead data. Blast radius measured in-process across the full 1583-9999 domain (throwaway probe, not committed): 16 350 days total across the six slugs, every one resolved via step 2 (Own_slug), zero anomalies, and the window's two Saturdays confirmed to contribute zero hits via this cause across the whole domain, corroborating the RG78-interception reasoning by direct sweep. Closed L2 in data/ef/expected-divergences-lms.sexp. New cited entries: C41 in data/ef/expected-divergences.sexp (95 of 16801 rows, 2005-2050 -- lectio has no equivalent rubric and still walks these ferias back); M31 in data/ef/expected-divergences-missalemeum.sexp (3 rows in the 2026-2027 window, plus the same shape independently reconfirmed in the 2035 and 2038 live-capture windows) -- missalemeum, Divinum Officium's lineage, still gives the same wrong answer colitur itself used to give, which is why these rows used to agree with M18's shape and now escape it (M18 373 -> 370, the same mechanism M23-M27 established when the citation axis was first built). Golden pin: 2031-05-23, the exact date the bug report was filed against. Mutation-tested: reverting data/ef/lectionary.sexp alone (a git-diff capture/restore, not a source-code revert, since the fix is data) reddens 19 tests across the differential, both oracle fixtures, all three LMS Ordo windows, the golden pin, the lectionary unit test and every Render golden template; restored and reconfirmed green (681 tests; 683 under COLITUR_EXHAUSTIVE_SWEEP=1, ~175s).
* fix(ordo): correct L5/F3 misadjudication -- Cum Sanctissima, not a gapLukasz Kasprzak2026-08-221-12/+209
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | data/ef/expected-divergences-lms.sexp's L5 (16 dates, LMS Ordo Gloria=true vs colitur's false) and data/ef/expected-divergences-fiuv.sexp's F3 (6 dates, FIUV Te Deum=true vs colitur's false) were both misadjudicated as colitur's own gap under an RG 302(b)/431(b) hypothesis. Both are wrong. These are occurrence differences from the 2020 decree Cum Sanctissima, which colitur excludes from its 1962 core by binding design (CLAUDE.md decision 2). RG 303(b) ("dici potest tantum si occurrit dies liturgicus IV classis") already prohibits 302(b) on the Class3 privileged ferias both entries cover -- the original text quoted 303(b) but never followed it to its conclusion. Decisively, the FIUV Ordo prints BOTH readings on the six shared dates, separated by a literal "-VEL-" token: the primary, strict- 1962 reading (feria wins, sine Gloria, no Te Deum) matches colitur exactly; the second, Cum-Sanctissima reading (saint kept, Gloria, Te Deum) matches the LMS Ordo exactly. F3 itself was never a real Te Deum finding: tools/extract_fiuv_ordo.ml's extract_te_deum scanned each block unbounded for "Ad Mat.", so on these six days -- whose primary office states no "Ad Mat." of its own -- it silently read the second office's own Te Deum instead. Fixed with a new split_at_alt_office, cutting every block at "-VEL-" before any field extraction runs; the alternative is captured, not discarded, in a new alt_te_deum field. A hard characterisation probe asserts the split fires on exactly the six known dates. Auditing for the same trap elsewhere (this is the third such manufactured divergence in this project) found a related but narrower marker, "Vel (sec. decretum <<Cum sanct.>>) Missa ...", offering an alternate Mass formulary within a single office. It never corrupted Gloria/Credo (always stated before the marker on all 20 real instances, confirmed by diff), only the uncompared praef field; hardened defensively anyway, before it could ever fire live. The LMS Ordo (all three editions) carries no equivalent dual-print mechanism at all. expected-divergences-missalemeum.sexp's M27 is the same genus but was already correctly adjudicated, not touched. Both allow-list entries are rewritten, not deleted: the original SHAPE/ CITATION/WHY-NOT-FIXED-HERE reasoning is preserved verbatim as a recorded, superseded hypothesis, matching this project's own correction convention. test_lms_ordo.ml's matching predicate is unchanged (the 16-date population still fires, and always will, because it reflects a standing exclusion, not a bug); only its citation comment is corrected. test_fiuv_ordo.ml's F3-specific predicate/count assertion is removed, matching this file's own prior F2/F4 closure discipline. lib/ is untouched. dune test: 676/676. make check (COLITUR_EXHAUSTIVE_SWEEP=1, full 1583-9999 domain): 678/678.
* feat(ef): the Gloria in excelsis, RG 431-432, deferring to Breviary 237-238Lukasz Kasprzak2026-08-221-0/+21
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Phase 2 of celebrant-rubrics-phase1: colitur rubrics gains a fifth column, whether the Gloria is said. Follows the Creed's own seam exactly -- Rite.t.gloria, Liturgical_day.t.gloria, wired through calendar.ml the same way. RG 431(a)/432(a) defer the Gloria to the Breviary's own Te Deum rule (nn. 237-238), so te_deum is implemented as its own named predicate, cited clause by clause, not collapsed into a colour heuristic. 431(c) (Holy Thursday, the Easter Vigil Mass) and 432(b)/(d) (violet; a Requiem) are independent overrides checked ahead of the Te Deum-derived answer. Every clause this engine has no dimension to model (votive Mass classes, the wider n.302 "Missa festiva" categories) is stated as N/A with its own reasoning, not silently dropped. Validated against the FIUV universal Ordo (Gloria and Te Deum) and all three LMS editions (Gloria). A first pass over-trusted a clean-looking 15-for-15 FIUV contradiction of 237(b)'s own Septuagesima exception and replaced it with a blanket "every Sunday" rule; the evidence was itself corrupted -- the FIUV extractor recognised only one of the source's two Te Deum negations ("non dicitur", not "sine"), so every "sine Te Deum" Sunday read wrongly true. Fixed in tools/extract_fiuv_ordo.ml, fixture re-extracted, and the literal 237(b) reading restored once the corrected data confirmed it. A second bug surfaced alongside it (Palm/ Passion Sunday wrongly reading true via Temporal_ef.named's own table membership, then Christ the King wrongly reading false from an over-broad fix) is closed with an explicit two-slug exclusion. Domain-wide 1583-9999: every violet or Rose day is gloria=false except the Easter Vigil (RG 431(c) lex specialis), every Requiem is gloria=false, both measured exhaustively, zero exceptions. Mutation- proved: disabling 431(c) reddens 8 tests including all four oracle comparisons; disabling 238(c)'s feria-I-classis exclusion reddens exactly the dedicated Ash Wednesday unit test, a genuine blind spot in both oracle layers, reported rather than hidden. Two open, cited findings, neither fixed here (out of this task's "follow creed's exact seam" scope): a privileged Lenten/Passiontide feria carrying one commemoration reads Gloria=true in the LMS Ordo but Te-Deum=true/Gloria=false in FIUV -- the two oracles disagree with each other, not merely with colitur (data/ef/expected-divergences-lms.sexp L5, expected-divergences-fiuv.sexp F3); and a pre-existing, uncited Colour.Violet bug on Rogation Monday/Tuesday in Temporal_ef.temporal, surfaced by this comparison but root-caused as a separate defect (L6). day/readings verified byte-identical against a build from the branch tip before this task (v0.10.1's own tag predates an already-landed bissextile fix that legitimately changed both, so it is not the right baseline). 671 tests green (dune test); 678 with the exhaustive sweep (COLITUR_EXHAUSTIVE_SWEEP=1 dune test --force, ~104s).
* test(fiuv-ordo): add the FIUV universal Ordo as a second, independent witnessLukasz Kasprzak2026-08-222-0/+651
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Wires in the FIUV (Foederatio Internationalis Una Voce) Ordo for 2025-2026 as validation layer seven -- the first UNIVERSAL (not diocesan) source in this project, and the first compiled by someone other than the LMS editions' own Peter Day-Milne (Joseph Shaw, FIUV President). tools/extract_fiuv_ordo.ml hand-parses pdftotext's -layout dump of the Latin-language ordo (no Str/regex, frozen deps). Three real parsing traps were found and fixed by tracing actual false results against the raw source, not assumed in advance: - "Gloria" is also the first word of "Gloria Patri" (the psalm doxology), which can appear INSIDE the very Mass clause being scanned (Good Friday: "...omittuntur ps. Iudica me et Gloria Patri), Gloria, sine Credo..."). Guarded by rejecting any "Gloria" hit whose next token is "Patri". - Christmas Day's own rubric uses the PLURAL "Missae"/"Missae" ("tres Missae celebrantur..."), never the singular "Missa" every other real day anchors on -- silently produced gloria=None/credo=None on the one most doctrinally unambiguous day in the calendar until traced and fixed, guarded in turn against "Missae defunctorum" (an unrelated Requiem-prohibition notice appearing at the end of many day-blocks). - The Vespers boundary (bounding the primary Mass option before a second "Vel Missa" alternative or before Vespers information) missed the ligature spelling "VESPERÆ", the dominant form in this source, letting a whole day's trailing prose leak into the captured [praef] field on the task brief's own worked example day (19 September). A distinct second stop-marker bug: the malformed trailing entry this source shares structurally with the LMS PDFs (a stray, mislabelled duplicate of 1 January) was initially over-matched by a generic "first token is 4 digits" heuristic, which false-positived on an indulgence-decree date citation wrapped mid-sentence in October ("...20 augusti / 1885, 26 augusti 1886..."), truncating the extraction to 308 of 400 real days. Fixed by anchoring on the tail entry's own distinctive "1st Jan" opening instead. Five opposite-prediction Creed probes are re-verified by the tool itself against its own extracted rows (a failed probe is a hard exit, not a printed claim) before the fixture is written. test/test_fiuv_ordo.ml compares the Creed against colitur across all 399 comparable days (Holy Saturday excluded, no Mass that day) -- CLEAN, zero unexplained divergences, a genuine second confirmation of {!Rite_ef.Rubrics_ef.creed} independent of the LMS lineage. [praef], [te_deum] and [class_] (including the FIUV ordo's own "III cl." vs "III cl. (Priv.)" Cum Sanctissima convention, stated on its title page) are captured into the fixture but deliberately not compared -- colitur has no Gloria/preface predicate yet, and its default configuration models no Cum Sanctissima overlay to compare the class convention against. All 634 tests green (dune test), 635 with the exhaustive sweep and make check. No lib/ file touched. Claude-Session: https://claude.ai/code/session_017ZBxCCRM2ojnBupp3SBxV9
* test(lms-ordo): extend the LMS witness layer from one year to threeLukasz Kasprzak2026-08-221-62/+189
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Generalises tools/extract_lms_ordo.ml's start-of-body detection: the hardcoded "End of November 2024" sentinel assumed every edition opens with an "End of November" stub section. The 2023-2024 edition has none (Advent Sunday 2023 fell on 3 December, so the compiler folded the two tail days into the "December 2023" header directly) -- the generalised detector anchors on the body's own "ORDO" title line instead, verified unique in all three known editions and distinct from the Table of Contents' lowercase "Ordo" entries. Extracts test/fixtures/lms-ordo-2023-2024.sexp (397 rows) and lms-ordo-2025-2026.sexp (399 rows), each with a provenance header carrying its own SHA-256, source citation, and a per-edition characterisation record: five opposite-prediction Creed probes, independently re-verified against this run's own extracted rows (a failed probe is now a hard die, not a printed claim). The 2025-2026 edition needed a different sharp II-class pairing than 2024-2025's Joachim/Stephen one, because Joachim (16 August) is impeded by a Sunday that year -- recorded in the fixture header, not silently swapped. test/test_lms_ordo.ml is refactored into a parameterised make_suite, instantiated once per edition; every count Task 6 hardcoded against the single 2024-2025 window is now a per-edition parameter, several of which turned out to differ once measured (BVM-Saturday population 12/14/13, Ascension-week exclusion 1/3/2). Two mapping gaps surfaced only by the new windows and fixed here (test-side, not colitur): Low Sunday reached via Preceding_sunday for the first time (absent from 2024-2025's own window), and the Ascension-week exclusion was wrongly keyed off week number alone -- Rogation Monday also resumes week 6's Sunday but falls BEFORE Ascension, not after, and the Ordo prints the ordinary Sunday text there. Re-bounded by date, against Ascension and Pentecost. Two genuine divergences surfaced in the 2023-2024 window and are allow-listed (L3, L4) rather than fixed, per this task's no-lib-changes scope: - L3 (verdict ordo): colitur has no bissextile calendar rule. The 1962 Missal's own calendarium moves St Matthias from 24 to 25 February in leap years (docs/research/LT.txt:5011); colitur keeps him on the 24th every year. Domain-wide upper bound measured with a throwaway sweep: 1,650 of 2,041 leap years in 1583-9999 currently observe him there. - L4 (verdict open): Christmas Eve falling on the 4th Sunday of Advent (2023-12-24, roughly 1 year in 7). colitur says the Creed is said, reading RG 475(a)'s Sunday clause; the Ordo says it is not. RG 30(a)'s own "locum tenet ... nulla fit commemoratio" for this exact vigil reads like RG 16(a)'s Feast-of-the-Lord-on-Sunday shape, which colitur already treats as Creed-preserving -- but RG 475(a) names "festo" specifically, and a vigil is its own liturgical-day category (RG 35), so the counter-reading is real too. Left open rather than adjudicated. All 630 tests green (dune test), 631 with the exhaustive sweep. Claude-Session: https://claude.ai/code/session_017ZBxCCRM2ojnBupp3SBxV9
* test(ef): the LMS Ordo as a sixth validation layerLukasz Kasprzak2026-08-222-0/+435
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | An independent lineage -- neither Divinum Officium, missalemeum nor lectio -- covering the Mass formulary and the Creed. Characterised before use, per the discipline the extraordinaryform.org Ordo's blanket St Lawrence vigil gap taught this session: five opposite- prediction Creed probes (Sunday/feria, octave-override vs plain II-class, I-class) all confirmed against the raw extracted text before a single divergence was adjudicated, recorded in the fixture's own provenance header. tools/extract_lms_ordo.ml hand-parses pdftotext -layout output (no Str/regex, frozen deps) into test/fixtures/lms-ordo-2024-2025.sexp, 400 day-rows (2024-11-27..2025-12-31). Gl/Cr print per Mass-option, not per day; the extractor takes the first pair in reading order, which the source's own layout guarantees is the universal entry's, since a diocesan variant always follows it, never precedes it. The Ordo's I-V roman numeral ("Missae de sancta Maria in sabbato", RG 309(a)) is matched whole-line only, never by substring -- "V Mass of BVM" is a substring of "IV Mass of BVM". test/test_lms_ordo.ml compares two axes 1:1 by date: - the Creed (RG 475-476): 399 of 400 days comparable (Good Friday has no Mass at all, asserted as the only such day); one genuine divergence, adjudicated to the ORDO, not colitur -- All Souls' Day 2025-11-03 is a Requiem Mass, and colitur's own creed predicate has no notion of one, a pre-existing scope gap rubrics_ef.ml already documented in its own header before this task existed. - the BVM-Saturday seasonal Mass selection, the addendum's own "sharper check": all 14 Votive days in the window match their season's own numeral exactly, both directions, independently re-derived from RG 309(a) rather than calling colitur's private function. - the formulary override, for the three sources it can honestly discriminate (Proper 179, Preceding_sunday 66, Common 2, all clean); Own_slug (139) is excluded with evidence, not silently -- several ferial slugs carry citations byte-identical to their Sunday's own, so the Ordo's override line cannot distinguish the two representations. data/ef/expected-divergences-lms.sexp carries the one adjudicated entry (L1), cited to RG 476(f). Mutation-tested: inverting RG 475(a)'s Sunday clause reddens 10 existing Rubrics_ef unit tests AND this new layer's own comparator, independently, on a data source none of the other five layers touch. Reverted after confirming. No lib/ changes. day/readings/rubrics output re-confirmed byte- identical (1583, 2026, 2038, 9999) after this task; all three gates green (dune test, exhaustive sweep, make check).
* fix(tools): close five more ways to defeat check_citations.pyLukasz Kasprzak2026-08-192-145/+782
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Round 1 hardened check_citations.py against its own self-poisoning bug; a review defeated it again. Five fixes, in the order they were found: 1. PATTERN silenced a whole comment block, not just the entry it was attached to -- a wrong citation on a DIFFERENT, unmarked entry in the same block (e.g. [season]'s own back-to-back trailing-comment style) was never checked at all. Fixed by scoping PATTERN with the identical leading/trailing pooling rule citations already use: an entry is excluded only by its own marker, never a neighbour's. 2. Explicit per-citation ranges (introduced in round 1 to replace a blanket +-2-line tolerance) had no upper bound, reintroducing the same defect at a much larger radius (LT.txt:8600-8650 passed if the text appeared anywhere in fifty lines). Capped at MAX_RANGE_WIDTH (3 lines); anything wider is reported MALFORMED, naming the entry and the width, instead of silently accepted. 3. The "pool too thin to verify" gate counted words, not rarity -- it flagged 11 genuinely correct citations (short Latin hagionyms with only one non-stopword) CANNOT VERIFY, while a match on nothing but "classis" (507 occurrences) passed freely alongside three siblings. Replaced with a frequency table over the whole LT.txt corpus: a token's evidence is 1/(times seen), an item's evidence is its single rarest matched token (not a sum -- summing would let several merely-common words add up to "enough" between them, the same shape as the self-poisoning bug). 4. "LT.txt:12,459" (a comma typo for one number) parsed as two unrelated bare citations, 12 and 459, either of which could coincidentally match while the intended line was never checked. Detected as a thousands-separator-typo shape (a 1-2 digit token immediately followed by an exactly-3-digit one -- the only way a real LT.txt line number, which never exceeds 5 digits, splits under one comma) and rejected as malformed. 5. The self-test suite overstated its own coverage: of round 1's seven fixture cases, only two actually failed against the pre-round-1 script. Every test is now labelled REGRESSION or CHARACTERISATION, each verified by direct replay against the named prior version rather than asserted -- 14 of 33 are genuine regression tests. Both of the review's own defeats (block-wide PATTERN silencing, the 50-line range) are reproduced as dedicated fixtures and confirmed caught; both are also confirmed to slip through the pre-round-2 tool unchanged. Claude-Session: https://claude.ai/code/session_017ZBxCCRM2ojnBupp3SBxV9
* fix(tools): harden check_citations.py against its own self-poisoning bugLukasz Kasprzak2026-08-193-87/+514
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Reproduced the defect: reintroducing the exact historical citation bug (pointing class-1's citation back at LT.txt:12459, the value a prior fix round corrected away from) made the tool report "147 citations checked, 0 look wrong". The mechanism was that the corrective comment documenting the old bug quotes the wrong historical value, and the checker pooled every quoted phrase from the whole surrounding comment block, so citing the wrong line matched the comment explaining why it was wrong. Four changes: 1. The word pool for a citation is now scoped to the entry(ies) it is attached to only -- never to quoted text elsewhere in the comment. This is the direct fix for the self-poisoning bug. 2. A citation whose pool has fewer than two distinctive words (Latin liturgical headings are short and stopword-heavy) cannot discriminate the right line from a wrong nearby one. Such a citation is now reported CANNOT VERIFY and fails the target, instead of silently passing. 3. The blanket +-2-line tolerance is gone. A bare "LT.txt:N" is checked at line N only; a heading that genuinely wraps must say so explicitly as "LT.txt:N-M". The allowance moves into the data, where it is visible. 4. The tool gets its own test suite, tools/test_check_citations.py, with a synthetic fixture covering: a correct citation, off-by-one and off-by-three mismatches, an explicit wrap range, a degenerate pool, a PATTERN-marked entry with no citation, and a dedicated regression test for the self-poisoning case itself. Wired into `dune test` via a new (rule (alias runtest) ...) in tools/dune (a plain (test ...) stanza cannot run a Python script), so it runs with the rest of the suite, not only as a `make` target. Added a --file/--lt-file override to check_citations.py so the tool (and its own tests) can point at a fixture without touching the real lang/la.ini or docs/research/LT.txt. Confirmed the "SKIPPED, exit 0" behaviour for a missing docs/research/LT.txt is unchanged. tools/__pycache__/ (a stray artefact of this script, previously untracked and ungitignored) is now in .gitignore. Measured against the current lang/la.ini (another task is still landing its sanctoral entries on this branch): 15 of 275 citations now look wrong and 42 more cannot be verified, both far above the 0 the unhardened tool reported. Not fixed here -- the data pass is separate, once the sanctoral entries land.
* fix(lang): correct two wrong citation lines, add check-citationsLukasz Kasprzak2026-08-191-0/+232
| | | | | | | | | | | | | | | | | | | | | | | | | | | | Two of la.ini's LT.txt:<n> citations pointed at the wrong line -- the Latin itself was right, only the pinned line was wrong: - advent cited LT.txt:8631 ("Tempus Nativitatis"); the real "Tempus Adventus" heading is at 8609. - ef-christ-the-king and [rank]'s own citation both pointed near "Dominica ultima Octobris" (12459) when the text they actually quote, "D.NI NOSTRI JESU CHRISTI REGIS" and "I classis", sits two and three lines further down, at 12461 and 12462. ef-christmas-sunday-0 was marked PATTERN but LT.txt:8644 is the identical string verbatim -- relabelled as a direct citation, not constructed. Added tools/check_citations.py and `make check-citations`: for every LT.txt:<n> citation outside a PATTERN block, confirms a +-2-line window around line n actually contains the Latin text the citation claims, rather than trusting each of the 38 citations by hand. Follows check-schema/check-templates' own precedent -- docs/ is gitignored, so the target prints SKIPPED loudly and exits 0 when docs/research/LT.txt is absent, never a silent pass. The checker's own teeth are proven three ways: replayed against the pre-fix file it independently re-derives both corrections above; a fresh mutation (redirecting one citation to an unrelated line) is caught and reverted; the fixed file passes clean, 147 citations checked, 0 wrong.
* docs(tools): the lectio patch exporter cannot see temporal correctionsLukasz Kasprzak2026-08-181-0/+11
| | | | | | | | | | | | It compares colitur's sanctoral data against lectio's calendar ini, so a correction to a temporal day -- computed in code on both sides, present in neither file -- is structurally invisible to it. Not hypothetical. The Good Friday colour fix (RG 128(b) + RG 132, v0.4.0) was missed entirely by this tool when the eight-field patch was produced, and surfaced only by running clectio at the bottom of the chain and diffing its output against colitur date by date. The tool narrows the search; it does not close it.
* feat(tools): export the lectio EF correction patchLukasz Kasprzak2026-08-181-0/+91
| | | | | | | | | | | | | | | | | | | colitur has been downstream of lectio since its data was bootstrapped from it. These are the fields where that relationship should now invert: eight changes, each annotated with the colitur allow-list entry carrying its rubric. Deliberately a PATCH and not a regeneration. colitur carries 205 of the 327 Polish names lectio ships, so regenerating lectio's ini from colitur would silently drop 122 of them; colitur also uses a different slug vocabulary for Passion/Holy week and different Paschaltide week numbering. What colitur is authoritative for is the adjudicated fields, and only those are emitted. Comparing the two datasets field by field also caught a counting error in colitur's own register that no assertion in this project was watching for: six sanctoral entries carry subject = Lord, not the four recorded, because the regex that counted them truncated on the two longest Polish names. The RG 112(b) reachability measurement was re-run against all six and is unchanged at 0/0; both docs are corrected.
* fix(lectionary): the week after Trinity says its own assigned MassLukasz Kasprzak2026-08-171-1/+39
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | RG 299 located (scan1:1096-1098), which does two things at once. First it closes a standing gap: the ferial-Mass selection rule was recorded in the register as unconfirmed against the primary source, and step 3 said so in its own comment -- "lectio's own observed behaviour, not a confirmed Missal citation". It is confirmed now. "In reliquis feriis dicitur Missa dominicae praecedentis, nisi a rubricis aliter provisum sit", restated in the propers as "...nisi propria Missa assignetur". Second, that escape clause splits M26 shape 2 in half, which is why the shape was never resolvable as one thing. Shape 2(a), the week after Trinity, is fixed. Trinity is the temporal identity of its own Sunday, so a plain walkback lands on Trinity's Mass -- but the Missal prints a named formula for the following week, "Missa dominicae I post Pentecosten" (scan1:21758), precisely because the feast has taken that Sunday's own. Those ferias now resolve at step 2 from it and never reach the walkback. Shape 2(b), Christ the King's week, turns out not to be a bug at all. No proper Mass is assigned there -- checked directly, no rubric precedes that Mass heading (scan1:39163ff) -- so RG 299's default governs and "the Mass of the preceding Sunday" genuinely is Christ the King's own, which is what colitur emits and missalemeum does not. Kept open only because no rubric specific to that week was located either way; the verdict is now argued from the primary text rather than resting on oracle disagreement. No Thursday in the slug list: Corpus Christi is Easter+60, which IS that week's Thursday every year, so the ferial slug never exists. Found by the bootstrap's own reachability guard refusing to emit a key no Temporal_ef slug can match, not reasoned out in advance. Allow-lists: C35 opened (69 rows, verdict colitur -- lectio still walks back); C17 narrowed 406 to 397, nine Saturdays moving to C35 because they carry both a citation and a colour difference, with C35 gated to require the citation so a colour-only Saturday still falls to C17; M26 narrowed 30 to 27; M18 widened 352 to 355, the same three days keeping only their observed-identity difference. Register: section 6.11.
* test(oracle): a second oracle year, 2038, closing step 4's blind spotLukasz Kasprzak2026-08-171-4/+13
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Register section 6.7 recorded that step 4 of the reading chain -- the Common route -- had no external witness of any kind, and proved it by mutation: corrupting a Common citation left both the 16801-day differential and the 730-day oracle green. This closes that. 2038 is the only year in 2005-2050 in which two of the five Common-routed saints are the observed office (Perpetua and Felicitas on 6 March, Frances of Rome on 9 March). 365 days were captured live, one request per day, zero failures. A separate fixture, not more rows on the existing one. sources.md already records that the live endpoint has drifted from lectio's archived 2026-2027 snapshot; merging two versions of one source behind a single set of expectations would make any future disagreement unattributable -- calendar or drift, no way to tell. Nothing in the 2038 comparison is checked against the older fixture, and its provenance file says so, along with the fact that a live capture is not reproducible byte-for-byte on demand the way a snapshot-derived one is. 338 of 365 days match. 27 differ, every one of them in a named, already-adjudicated class, none unexplained: 13 the BVM Saturday votive Mass (M26 shape 1), 7 colour (M21), 2 Judith (M28), 2 Christ the King's week (M26 shape 2b), 1 Christmas multi-Mass (M27), 1 Perpetua's Common, 1 new. Each was decided in a different year, so 2038 re-confirms them independently -- the point of a second window is not new rulings but evidence the old ones are not artefacts of their own year. Two findings came out of it. Perpetua and Felicitas: missalemeum serves the Common of Virgins and calls them "Virgins and Martyrs" in its own oration, where the Missal directs "Missa Me exspectaverunt, de Communi non Virginum I loco" (scan1:27634-27635). Both Commons share the Introit Ps 118:95-96, which is exactly why the calendarium qualifies its direction, and both women were mothers. Verdict colitur; the mechanism is located on missalemeum's side, not merely asserted. Passion Tuesday: the Missal prints "Dan. 14, 27 et 28-42" (scan1:11106) and colitur reproduces that two-part form where missalemeum collapses it to 27-42. Same verses; the convention is deliberate, appearing also in the Seven Sorrows and the Common of Non-Virgins. Verdict colitur, cosmetic. Citations are notation-normalised before comparing, as layer 3 already does: Ecclus/Sir, Joann/John, Luc/Luke, Matth/Matt. Each pair was added because a real row needed it, and no target contains its own source as a substring, so the set is idempotent. Not compared, stated rather than left to be found: commemorations and observed-identity. That machinery is built around a date-literal 28-entry allow-list specific to 2026-2027, and re-deriving it for a second year is its own task. 2038 compares rank, colour, Epistle, Gospel. The extractor's day count was hardcoded to 730, which silently forbade any other window. It is now a parameter defaulting to 730, so the existing documented command keeps its guard and a partial fetch still fails loudly instead of producing a short fixture that passes a comparison it never ran. Teeth, by re-running section 6.7's own experiment: corrupting the Common of Non-Virgins II now reddens four tests, two of them external-oracle, where the same mutation previously reddened neither oracle layer. Residual: isidore-of-seville is still unwitnessed and needs 2035 or 2046; gregory-the-great and patrick are never the observed office in any year 2005-2050, so no fixture in that range can reach them. Register section 6.8 and 6.9.
* test(oracle): fix round 1 -- C30 disclosure, RG 17(a) misattribution, M26 shapesLukasz Kasprzak2026-08-172-43/+81
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Three Important findings from coordinator review of 8d1eb25, all fixed. 1. C30's own note claimed its 91-row population "is C25's OWN former population, before this task narrowed it" -- false. Independently re-derived against the real resolver (pre-fix vs post-fix binary) and the lectio fixture: only 44 of the 91 rows were ever in C25 (Wed 6/ Thu 19/Fri 19, matching C25's own fix-round-3 breakdown exactly); the other 47 (all of Monday/Tuesday, plus part of Wednesday/Friday) previously matched lectio outright -- both engines independently landing on the same wrong value -- and are newly allow-listed only because colitur is now right. Rewritten to the same "both engines silently agreed on the wrong answer" pattern C33/C34 already used. 2. "Diebus ferialibus" is not RG 17(a) -- RG 17(a) states only the day-identity privilege; the ferial-Mass rubrics are Mass-propers rubrics, a distinction the register's own §6.3 already drew before this task's first pass blurred it in code/allow-list comments. A further slip: C31/C32 called their own rubric "RG 17(a)'s own next/ third sentence" when it is a different rubric block ~230 lines away, in Holy Family's (not Holy Name's) own feast propers. Fixed the attribution chain throughout tools/bootstrap_lectionary.ml, data/ef/expected-divergences.sexp's C29-C34, and (gitignored, outside this commit) docs/research/rules-register.md -- closed the §6.3 park, added §6.5 recording all four citations with corrected attribution. Quotes and line numbers were already right; only the chain of custody to RG 17(a) was wrong. 3. M26's own 30 rows classify into two nameable shapes, not "three confirmed shapes" with an unexplained 23-row residue that was in fact empty: 21 rows are RG 78's own already-documented BVM votive-Mass gap; 9 rows are a second, distinct gap -- a week whose own Sunday was displaced by a movable named feast (Trinity->Corpus Christi, "Missa Dominicae I post Pentecosten"; Christ the King), where the Missal requires the displaced Sunday's own Mass to keep informing that week's ferias but step 3 walks back to the displacing feast's citation instead -- proven directly by colitur's own citation on the Christ- the-King rows being literally that feast's own Gospel. verdict open unchanged (not fixed here); the shape is now named instead of buried. Minors: section count corrected 1458->1456 (four places); "exactly one candidate, zero ambiguous" corrected to "correctness rests on ordering, not uniqueness" (2 of 1456 sections genuinely yield two candidates, Holy Saturday's own Lectio, both years); the stale "nothing in layers 3-5 compares citations" claim corrected (only layer 5 still doesn't); M18's note corrected (five new entries not six; ~18 of its remaining 352 rows only agree now because of this task's own fixes, not "already" before it); "five" corrected to "six" otherwise-unoccupied Saturdays in bootstrap_lectionary.ml; scan2 quotes softened where OCR-garbled. Also done outside the git-tracked files: the step-4 (Commons) coverage gap is now recorded in data/ef/expected-divergences-missalemeum.sexp's own header, not only the commit body and the (gitignored) report. dune test: 358 tests green. COLITUR_EXHAUSTIVE_SWEEP=1 dune test --force: 359 tests green, 95.7s.
* test(oracle): compare reading citations against missalemeumLukasz Kasprzak2026-08-172-51/+255
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Layer 4 (test_oracle.ml) now extracts and compares the Epistle/Gospel citation for every one of the 730 days in the 2026-2027 fixture, the same axis Task 8 already added to layer 3. tools/ extract_missalemeum_oracle.py gained a structural citation-span detector (the first "*...*"-wrapped span under 40 chars containing a chapter:verse-shaped digit pair), verified exhaustively over all 1 458 Lectio/Evangelium sections. 728 of 730 days are comparable; the 2 that are not (Good Friday, both years) have no such section in missalemeum's own JSON at all -- a structural limit on the oracle's own side (M23), not a colitur gap. 451 of 730 days differ and are all explained across 19 cited entries; 0 unexplained. Chain step 4 (the Commons) has no layer-4 witness: confirmed empirically (a scratch sweep, not merely re-asserted from Task 6's own report) that none of the 15 Commons/proper saints is ever the observed office in this window. The comparison surfaced three genuine colitur bugs in data/ef/lectionary.sexp, all fixed at the source in tools/bootstrap_lectionary.ml: 1. Holy Name of Jesus (ef-holy-name-sunday/ef-holy-name) carried a mis-derived citation, borrowed by analogy from an unrelated ini section ("ef-christmas-sunday-0", the Sunday-within-the-Octave's own Mass) on the wrong assumption the two share a Mass. Disproved by missalemeum's own extraction for both real Holy Name Sundays in its window, confirmed against the Missal itself, twice, both scans (Acts 4:8-12/Luke 2:21, not Gal 4:1-7/Luke 2:33-40). 2. RG 17(a)'s own "diebus ferialibus" rubric (immediately following the Holy Name heading, both scans) was never implemented: 2-5 January ferias repeat 1 January's Mass, 7-12 January ferias before Holy Family Sunday repeat Epiphany's, and Holy Family's own week has a named formula ("Missa Dominicae Primae post Epiphaniam"). All three fell to step 3's undifferentiated Sunday walkback instead. Fixed for three 5-weekday families, deliberately excluding Saturday from all three -- RG 78's BVM Saturday Office wins there unconditionally, confirmed against missalemeum's own title on this window's own such Saturdays. 3. Corpus Christi and Sacred Heart (both real, never-impeded I-class movable feasts) had no lectionary entry at all. Both engines independently fell back to Trinity Sunday's own citation and silently agreed on the wrong answer -- the "Holy Thursday violet in both" shape CLAUDE.md names for exactly this reason. Because lectionary.sexp is shared with layer 3 (the lectio differential, 2005-2050), these fixes changed colitur's output for the whole domain, not only 2026-2027. data/ef/expected-divergences.sexp and test_differential.ml needed matching updates: C1 narrowed 361->173 and C25 narrowed 57->13 (rows that now resolve correctly moved to new entries), C16's note corrected (count unchanged), and six new entries added (C29-C34, 397 rows total, all verdict colitur). test_lectionary.ml and test_lectionary_ef.ml's own pins were updated to match (entry count 131->148; one step-3 regression test re-pointed to a date that still genuinely exercises that step). One entry, M26 (30 rows, verdict open), is a genuine unfixed gap: colitur's step-3 "resume the preceding Sunday" fallback is its only ferial-Mass mechanism, and missalemeum's own selection is more granular in three confirmed ways (a rotating votive Mass of the BVM, a named weekly formula for at least one week, and later Time-after-Pentecost weeks this task's own investigation did not narrow to a single rule) -- the same "needs a season-keyed selection mechanism, new behaviour not new data" shape data/ef/expected-divergences.sexp's own C6/C25 and Task 6's own residual concern already name, out of this task's own scope to build. dune test: 358 tests green. COLITUR_EXHAUSTIVE_SWEEP=1 dune test --force: 359 tests green, 89.5s.
* test(differential): fix round 3 -- two more Ember Saturdays, doc fixesLukasz Kasprzak2026-08-151-7/+67
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Two Ember Saturdays shipped an apostle's Mass verbatim from lectio's own ini, the exact shape already found and fixed for Passion Tuesday (C26): ef-advent-ember-sat carried St Thomas the Apostle's Mass (Eph 2:19-22 / John 20:24-29, 21 December) instead of the real "Sabbato Quatuor Temporum Adventus" (2 Thess 2:1-8 / Luke 3:1-6); ef-september- ember-sat carried St Matthew's Mass (Ezek 1:10-14 / Matt 9:9-13, 21 September) instead of the real "Sabbato Quatuor Temporum septembris" (Heb 9:2-12 / Luke 13:6-17). Both verified against both primary-source scans independently. Both readings are explicitly labelled "Lectio Epistolae" in the Missal on both days, so neither needed the "last lesson before the Gospel" convention Holy Wednesday/Good Friday relied on -- a textual fact, not an editorial choice. Fixed at the source in tools/bootstrap_lectionary.ml: colitur_keys excludes both wrong ini sections; a new ember_saturday_corrections list hand-authors the real citations. New allow-list entries C27/C28 (40 rows each, not the naive 46 -- traced to the identical 6 civil years where 21 September and 21 December, always exactly 91 days apart, land on a Saturday and the fixed saint's feast wins outright). Both values pinned in test_lectionary.ml and mutation-tested. Also: C25's per-weekday breakdown was mislabelled (right multiset, rotated weekday labels) in both test_differential.ml and expected- divergences.sexp -- corrected to Wednesday 6/Thursday 19/Friday 19/Saturday 13, and the claim that this split came directly from the comparator's own output is narrowed to what's actually true (only the total, 57, does). C26's scan2 citation range was extended to include the Gospel (previously stopped after the Epistle). lib/kernel/liturgical_day.mli's "citations always empty until Plan 4" comment is now stale on this branch -- left untouched (Task 11's own documentation pass), noted in the task report so it isn't lost. dune test: 358 tests green; COLITUR_EXHAUSTIVE_SWEEP=1: 359 green.
* test(differential): fix round 2 -- Good Friday, Passion Tuesday, stale headerLukasz Kasprzak2026-08-151-57/+150
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Critical, and worse than what it replaced: removing a lectionary key does not mean "no reading" to Lectionary_ef.readings -- it means step 3 silently resumes the preceding Sunday. Good Friday, which has no Mass at all, was emitting Palm Sunday's own Epistle and Passion narrative every year; Holy Wednesday had the same defect. Fixed by authoring both days fully: the Gospel was always unambiguous and is now always authored; the Epistle uses the same "last lesson before the Gospel" convention this file's own Lenten Ember Wednesday entry already established for an identical two-peer-lesson shape, applied for consistency rather than invented to close this gap. C24 is retired, folded into a widened C23 covering all six Holy Week days. Important: Passion Tuesday was shipping Holy Tuesday's Mass, a defect the previous round's own footnote had found and left unrecorded. Fixed at the source -- colitur_keys now excludes lectio's own (wrong) ini section for this one weekday rather than translating it; a new passion_tuesday_entry hand-authors the real Missal citation (Dan 14:27, 28-42 / John 7:1-13, both scans). New entry C26 records the resulting divergence, 43 of 46 years, traced (not merely counted) to three years where a competing I-class feast impedes the day entirely. Important: the allow-list header was stale one level up, still naming and restating the diagnosis of an id (C21) retired in the previous round. Rewritten in full rather than layered with another correction. Minors: all three Lent Ember values now pinned (only Wednesday was before); C22's citation corrected to attribute the date-anchor text and the rank text to their own separate sources; assert_reachable's own comment no longer overclaims what it can catch; C25's falsified reasoning replaced with the directly observed fact (a first replacement derivation was itself found wrong on a second reading and is recorded as rejected, not silently discarded); hand-authored citations standardised to English book names with a colon, matching this file's own dominant convention, with one stated exception where a value must stay byte-identical to lectio's own data rather than be reformatted. dune test: 358/358. COLITUR_EXHAUSTIVE_SWEEP=1 dune test --force: 359/359.
* test(differential): fix round 1 -- Holy Week, Lent Ember, RG 69Lukasz Kasprzak2026-08-152-45/+272
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Two Critical findings, both real: Holy Week (including the entire Sacred Triduum) was reading Passion Week's Masses -- the original colitur_keys widened lectio's single, conflated ini section into both weeks instead of Passion week alone. Narrowed to Passion week only; Holy Week's own four cleanest days (Monday, Tuesday, Thursday, Saturday) are hand-authored directly from the Missal, verified twice against both scans (C23); Holy Wednesday and Good Friday are genuinely multi-lesson liturgies with no single Epistle slot and are deliberately left unanswered, not guessed (C24). The Lenten Ember days were still dead keys: Temporal_ef computes ef-lent-ember-{wed,fri,sat}, lectio's ini names the same data ef-lent-1-{wednesday,friday,saturday}, and colitur_keys never translated it -- both engines independently fell through to the same wrong ferial answer, so even the differential stayed green (C22, a genuine lectio bug recorded for upstream). Fixed the instance and the class: colitur_keys gained the rename, and tools/bootstrap_lectionary.ml gained assert_reachable, sweeping Rite_ef.Temporal_ef.temporal at generation time so a translation table entry naming a slug colitur never computes dies loudly instead of shipping silently, plus an informational log of every real slug with no entry at all. C21 ("colitur's step 3 cannot reach a correct answer for the fixed Christmastide slugs") is retired, superseded rather than merely patched: the Nativity-Octave days now have their own direct Missal formulary (Tit. 3,4-7/Luc. 2,15-20, "Diebus infra octavam Nativitatis Domini"), resolved at step 2, not step 3 at all. That surfaced a real RG 69 gap underneath it: a Sunday landing 26-31 December should keep its own Office, not the fixed weekday placeholder, which Temporal_ef does not currently distinguish. C6's verdict is re-opened (open, not colitur) for the 19 of its 138 rows where this applies directly, and a new entry C25 (57 rows) records the same gap propagating one hop further through step 3 into Holy Name week. Both are out of this task's own scope -- a behaviour change to a shared, multi-round-reviewed kernel-adjacent function -- and recorded, not fixed, per the coordinator's own instruction. C6's own note is rewritten in full rather than layered with another correction; C18's stale trailing count is fixed to match its own pin; the Vigil-misapplication risk flagged as a Minor closed itself once the Nativity-Octave fix landed, confirmed by inspection rather than assumed. dune test: 358/358. COLITUR_EXHAUSTIVE_SWEEP=1 dune test --force: 359/359.
* test(differential): compare reading citations, 16 801 daysLukasz Kasprzak2026-08-151-9/+150
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Layer 3 is NOT blind to the ferial resumption rule, unusually for this project: lectio implements the same rubric independently, so the differential checks colitur's rule against a second implementation rather than against silence. It IS blind to the Commons -- lectio has no such concept -- so chain step 4 rests on layer 4 alone. Divergence triage, 1 532 of 16 801 days differ (15 269 match exactly), 0 unexplained: - A genuine colitur bug found and fixed: data/ef/lectionary.sexp carried lectio's own ini section names verbatim, never translated into the slugs Temporal_ef actually computes for the identical office. Data the bootstrap already held (Passiontide both weeks, the Pentecost Ember days, Holy Name Sunday) was unreachable by chain step 2, silently falling through to a wrong step-3 answer -- invisible until citation content became comparable. tools/bootstrap_lectionary.ml gained colitur_keys (rename/widen), a hand-authored ef-nativity-vigil entry cross-sourced from lectio's own sanctoral calendar and corroborated by colitur's own (overlay-suppressed) sanctoral bootstrap of the same source, and a derived ef-holy-name entry (RG 17(a)'s single Mass heading covers both the Sunday and 2-January shapes). One guess (giving the fixed Nativity-Octave days Holy Name Sunday's own citation uniformly) was tried and reverted once measured against the fixture -- Missal-verified to be right only for the genuine "Sunday within the Octave" case, wrong whenever the true preceding Sunday is Christmas Day or Advent IV instead. - 10 existing/widened Layer C entries (C1/C6/C8/C14/C15/C16/C17/C18 -- C6 and C17 narrowed with the removed population accounted for, not silently re-absorbed) plus two new ones: C19 (the already-adjudicated Time-after-Epiphany week-numbering offset, now visible in citation content), C20 (lectio computes no reading at all for the seven readingless saints Task 6 gave a Common or proper -- colitur is more complete, not wrong). - One entry, C21, is a genuine colitur LOGIC limitation found but not fixed: Lectionary_ef.readings' step 3 is a single hop to the preceding Sunday, insufficient for a run of colitur-only fixed Christmastide slugs whose own preceding Sunday is itself another entry-less fixed slug. Recorded verdict open (data/ef/expected-divergences-missalemeum.sexp's own established precedent for this shape), not left silently failing -- fully diagnosed in task-8-report.md, recommended as a dedicated follow-up task rather than a hasty fix to a shared, multi-round-reviewed kernel-adjacent function.
* data(ef): carry sanctoral propers on the celebrationLukasz Kasprzak2026-08-151-2/+26
| | | | | | | | | | | | | | Celebration.citations has existed in the type, the .mli and the sexp schema since Plan 2 and has been emitted as () for every entry; the bootstrap now writes the real values for the 208 entries lectio has them for. On the celebration rather than in the lectionary table deliberately: a diocesan overlay adding a saint then carries his readings through the existing overlay algebra, with no second file to edit and no new directive. Asserted both ways -- the 13 January proper is present, and no Commemoration_only entry has readings, because in the EF a commemoration contributes an oration, not a reading.
* data(ef): bootstrap the temporal lectionary from lectioLukasz Kasprzak2026-08-142-0/+113
| | | | | | | 119 entries, Epistle + Gospel only. The generator refuses a section carrying exactly one of the two: an Epistle without a Gospel is malformed and wants investigating, not shipping. SHA-256 of the source INI is in the provenance header, as with sanctoral.sexp.
* tools(bootstrap_sanctoral): fix round 1 (F6) -- stale entry-count commentsLukasz Kasprzak2026-08-121-8/+18
| | | | | | | | | | | Three comments still said "322" after the previous commit's regeneration moved the total to 327: the date-form comment (rewritten to not carry a count at all, since it is about a FORM, not a quantity); the explicit-class comment (327, plus recorded that the 6 entries carrying it changed membership -- 12 September dropped it, 13 January gained it -- even though the count of 6 itself did not move); the name.pl comment (327 total, 5 missing it, 322 carrying it -- was "5 of the source's 322 entries", conflating the new total with the old one).
* tools(bootstrap_sanctoral): name.pl is optional, not requiredLukasz Kasprzak2026-08-121-2/+17
| | | | | | | | | | | | | | | | | lectio's regenerated tridentine-calendar.ini (the fix that corrects the 15-entry rank cluster and adds the 5 missing entries this branch re-bootstraps from) has 5 entries with no name.pl field at all (agnes-secundo, boniface-martyr, eusebius-confessor, evaristus, theodore -- all either newly added or newly un-hidden). parse_names previously required name.pl unconditionally (`field sec "name.pl"`, which dies on absence) -- correct for a source that always had it, wrong now that it legitimately does not. Names.t is an open, per-language assoc list (names.mli) with no rule that every entry must carry every language colitur happens to know about, so requiring pl here was this tool's own invented constraint, not a kernel one. Falls back to an en-only Names.t when pl is absent, never a placeholder string.
* feat(test): oracle layer compares commemoration identity, not just countLukasz Kasprzak2026-08-121-3/+18
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | test_oracle.ml (validation layer 4) used to compare commemoration presence and List.length only -- cardinality, never which candidate is admitted. A reviewer proved this vacuous by reversing admit's dignity sort (the engine admitting the worst commemoration, an outright RG 111 violation) and finding every assertion in this file green across all 730 days. tools/extract_missalemeum_oracle.py now also emits each commemoration's own missalemeum id (field 9, commemoration_ids), parallel to the existing title field; fixture regenerated from the same underlying lectio snapshot (commit d7da4b0, re-verified, first 8 fields byte-identical), read from a scratch copy so ~/git/projects/lectio was never touched. SHA-256 pin and provenance updated. The mapping: colitur identifies a commemoration by slug, missalemeum by an English title string. No hand-built slug<->title table -- colitur's own Celebration.t.names already carries the same English text for every SANCTORAL-origin commemoration (bootstrapped from lectio, verified to match missalemeum's titles character-for-character). This resolves identity for every sanctoral-origin commemoration but cannot resolve a TEMPORAL-origin one (Rite_ef.Temporal_ef never sets a celebration name) -- that case is a new, distinct outcome (Comm_identity_unresolved), counted and allow-listed, never silently skipped: 19 of 227 non-empty-commemoration days over 2026-2027. Two genuine identity mismatches found and adjudicated (new allow-list entries M15-M17, one is the unresolved bucket): M16, 27 March 2026, the already-known unimplemented Seven Sorrows of Passion Friday office (verdict missalemeum); M17, 22 September 2027, the RG 113 same-band tie-break residual (verdict open, per the sibling admit fix). A new corroborating check cross-references missalemeum's own id-embedded rank against colitur's, split by status: Feast-status matches must agree exactly (none reachable in this 2-year window, checked regardless); Commemoration_only-status matches either genuinely agree or fit a known, pre-existing, already-registered convention gap (colitur's bootstrap rank vs missalemeum's own sentinel) -- not a title-matching defect. Perturbation test performed and reverted: reversing admit's dignity- based ordering (compare_precedence's real committed form uses band, not dignity -- this reproduces the historical defect shape directly) turns the oracle layer red: "no differences outside the cited allow-list" fails with an unexplained commemoration-identity-mismatch on 2026-09-23 (colitur admits "St. Thecla" where the rubric and missalemeum both require "St. Linus"). Reverted; diff against the prior commit confirms no residue. CLAUDE.md's "know what each layer cannot see" section updated: the commemoration-identity gap it documented as open is now closed, with the mapping's limits and the RG 113 tie-break finding summarised. 271 -> 273 tests (274 with the exhaustive sweep), all green.
* test: oracle vs missalemeum 2026-2027; audit the sanctoralLukasz Kasprzak2026-08-121-0/+87
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Validation layer 4: an oracle harness against missalemeum (Divinum Officium data), independent of the lectio bootstrap chain colitur's own sanctoral data comes from -- the only layer that can catch an error inherited from that bootstrap, and the only one that can validate commemorations at all (the lectio differential explicitly excludes them, per its own header comment). tools/extract_missalemeum_oracle.py shapes the fixture from lectio's sources/snapshot.tar.gz outside the test (no JSON library in this project's frozen deps, same reasoning test_differential.ml's own fixture already documents). test/fixtures/missalemeum-ef-2026-2027 .txt (730 days, SHA-256 pinned and asserted) + its own .provenance note record exactly how to regenerate it. test_oracle.ml compares three axes the oracle actually supports: rank, colour (SET MEMBERSHIP -- 14 of 730 days carry two colours, e.g. rose+violet on Gaudete/Laetare, which independently vindicates this project's own rose reading against lectio's violet-only one, recorded in the register), and commemoration presence/count. Slug identity is deliberately out of scope (needs a title->slug mapping, the data audit's own business, not the automated comparator's). Of 730 days, 688 matched cleanly outright. The remaining 42 are all named in data/ef/expected-divergences-missalemeum.sexp (14 cited entries, M1-M14): most are genuine primary-source-confirmed findings this task adjudicated and fixed in the two preceding commits (RG 33, RG 109/111, Holy Thursday's colour); the rest are real, cited, deferred feature/data gaps (RG 91 entry 27's BVM-Saturday office, RG 110's inseparable Peter/Paul commemoration, four sanctoral entries missing from lectio's own source) or genuine oracle-side artifacts -- honestly verdicted against whichever side this task's own primary-source research actually backs, never defaulted to colitur. One entry (M13, St Joseph vs the Friday of Passion Week 2027) is verdict open: adjudicated as unresolved after real search effort, not guessed past. The data audit: every sanctoral entry the comparison flagged was hand-checked against the 1962 calendarium, plus a 20-entry deterministic random control sample (seed 20260812) drawn independently of the flagged set. The control sample caught two entries (benedict, frances-rome) marked Commemoration_only in the bootstrapped data when the primary calendarium lists them as plain III-class feasts with their own Office -- traced to lectio's own source, not fixable here, and reported as a signal (10% of a random sample) rather than a blanket claim. Coverage recorded honestly in docs/research/rules-register.md's own three buckets: confirmed by oracle (200/322), confirmed by hand (23/322, 2 of them wrong), unverified (115/322) -- the unverified bucket stated explicitly rather than left implicit. Harness teeth demonstrated and reverted (not committed): a fixture rank/colour edit on a previously-clean day fails both the checksum pin and the no-unexplained-differences assertion independently; an expected_rows drift on the allow-list fails the citation-count assertion. Both captured with their exact failure messages, both reverted before this commit.
* data(ef): bootstrap the 1962 sanctoral from lectioLukasz Kasprzak2026-08-122-0/+245
Convert lectio's tridentine-calendar.ini (322 entries) into data/ef/sanctoral.sexp via a validating OCaml converter, tools/ bootstrap_sanctoral.ml, rather than a hand-written script: every field is built through Slug.of_string, Colour.of_string and Vocab_ef.rank_of_string, so the emitted sexp is valid by construction. Two conversion decisions, both documented rather than buried: - subject defaults to Subject.Saint, overriding Celebration.make's kernel default of Subject.Temporal, for the 316 entries with no explicit class; - rank = commemoration maps to status = Commemoration_only with an inferred Class3 (not a citation -- it is what the 1960 reform reduced most simple feasts from), recorded as an open item in the rules register for the oracle to adjudicate. Every celebration is tagged layer = Precedence_ef.universal_layer, the provenance id RG 91's band classifier reads to tell the universal calendar from proper/indult data. The generated file carries a provenance header: source path, its SHA-256, and the UTC conversion date, so re-bootstrapping against a newer lectio is reproducible and diffable. Output is byte-identical across runs. test/test_sanctoral_ef.ml loads the file through Layer.load and checks the counts independently derived from the source INI (322 entries, 114 Commemoration_only, 12 Class1, no Subject.Temporal, every date resolves in a leap year), plus two named spot-checks against the INI's own text -- one entry with an explicit class field, one commemoration -- so a passing count cannot hide the wrong 322 entries having been converted.