aboutsummaryrefslogtreecommitdiff
Commit message (Collapse)AuthorAgeFilesLines
* test(ef): compare the LMS Ordo's formulary override STRING, not just presenceLukasz Kasprzak2026-08-221-13/+207
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | test_lms_ordo.ml's formulary-override check only asked WHETHER a Preceding_sunday day showed a "Mass of ..." override line, never WHICH Sunday it named. colitur could have resumed the wrong Sunday on all 66 such days and this layer would have stayed green -- resuming the right Sunday is precisely what step 3 of the reading chain exists to do. test_formulary_override_matches now derives the Ordo's own expected prose from Mass_formulary.t.said's own resolved slug (parsed directly, no Str/regex) and compares it against the Ordo's real text. Two narrow, evidenced exceptions, both counted rather than silently absorbed: - Christ the King's own Sunday reaches this population under a NAMED slug (ef-christ-the-king) that carries no embedded ordinal; its position among the Sundays after Pentecost varies by year, so Temporal_ef.week is read for that one slug specifically, not called generically (an earlier version of this fix read [week] for every day and was wrong -- caught by its own assertion on the very first run, because Time_after_pentecost's own last-Sunday and surplus-Sunday renumbering diverge from the raw week count. Reading the number back out of the already-resolved slug sidesteps both special cases and is not tautological with the wrong-Sunday failure mode this fix exists to catch). - Three real days between Ascension and the following Sunday (2025-05-30, 2025-06-02, 2025-06-03) are a genuine, newly-found defect: colitur resumes the nearest calendar Sunday, but the Missal, and the Ordo, keep saying Ascension's own Mass through this stretch instead. Structural (Ascension is a fixed Easter+39), not a fixture artefact, and not fixed here -- it needs a lectionary-data change out of this fix round's own scope. Documented in expected-divergences-lms.sexp as a prose-only note (no active sexp record: this file's id-lookup machinery is scoped to the Creed axis, and a second axis reusing it would let one test's "declared but never fired" check spuriously trip on the other's entry) and enforced directly by this test's own count assertion instead. Mutation-proved: offsetting step 3's Sunday lookup by an extra 7 days reddens this test (23 failures total across the suite: layers 3-6, golden pins, and lectionary_ef's own unit tests), confirmed via the compiled test binary directly; reverted clean, full suite green again.
* fix(ef): RG 476(f) -- the Creed is not said at a Requiem MassLukasz Kasprzak2026-08-225-45/+247
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | colitur said the Creed IS said on All Souls' Day and every other Requiem-shaped day: RG 475(b)'s "in festis I classis" branch fired on rank alone, with nothing checking RG 476(f), "Non dicitur symbolum ... f) in Missis defunctorum". Allow-listed as L1 in expected-divergences-lms.sexp on the argument that [creed] has no notion of a Requiem Mass in its inputs and could not express the rule without a signature change. That argument does not hold. Colour.Black is used by exactly two celebrations in the whole engine, verified by grepping every occurrence in lib/ and data/: commemoration-of-all-souls (data/ef/sanctoral.sexp) and Good Friday (temporal_ef.ml, already false via the earlier RG 23(b) feria exclusion and has no Mass at all in the 1955-restored Holy Week). So on every day this engine can construct, colour = Black if and only if the Mass is a Requiem -- a citable implication (RG 117 assigns black to Masses of the dead) on this data, not a heuristic. Rite_ef.Rubrics_ef.creed now reads observed.colour as RG 476(f)'s own guard, positioned ahead of 475(b)'s rank branch. A new test, test_colour_black_population_is_exactly_two, sweeps both the shipped data and Temporal_ef's own code across a century and fails loudly the day a third Colour.Black celebration is ever introduced, so the proxy cannot silently rot. L1 is closed in expected-divergences-lms.sexp, citation preserved. Blast radius, measured (full 1583-9999 sweep, grouped by observed slug): 8 416 days change from true to false, every one commemoration-of-all-souls (missing from exactly 1 of the 8 417 domain years, a boundary-year transfer effect at 1583/9999). A further 8 416 Colour.Black days are Good Friday, unaffected (already false before this fix). colitur day/readings stay byte-identical to v0.10.1 (checked directly against a v0.10.1 worktree build, 9 domain-spanning years); colitur rubrics changes on exactly the 8 416 days above. colitur emit --format sexp's pretty-printed line count for 2027 moved 9010 -> 9011 as a side effect: Sexplib.Sexp.to_string_hum wraps by column width, and All Souls' Day 2027's record grew one wrapped line because "false" is one character longer than "true". Cosmetic only -- no field's data changed -- and pinned in test/cli.t with the measurement recorded.
* test(ef): the LMS Ordo as a sixth validation layerLukasz Kasprzak2026-08-227-1/+2474
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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(ef): check RG 24/25's ferial invariant instead of asserting itLukasz Kasprzak2026-08-222-11/+215
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | rubrics_ef.ml's Creed check excluded feriae from 475(b)/(c)/(e) via an explicit test for feria I classis (Ash Wednesday, Holy Week) and vigils only. RG 24/25's feriae II and III classis (Advent 17-23 December, the Advent/Lent/September Ember sets, the numbered Lenten/Passiontide ferias) were excluded by no check at all, correct today only because of an unstated Temporal_ef property (no ferial-origin office it builds carries subject Lord/Bvm or an apostle-list slug). A comment read as if this were established "by construction of the two exclusions immediately above", which overclaimed: those two exclusions cover feria I classis only. Corrected the comments to name RG 24/25 explicitly, state what is and is not checked, and cite where the real guarantee comes from. Added a test that sweeps Temporal_ef.temporal across a century (1583-1682), asserting the invariant directly against every ferial-origin day found, with its own coverage counters confirming the span actually reaches Advent 17-23, all three non-Whitsun Ember sets, and Lenten/Passiontide ferias. The first run of the test found a real gap in its own population filter (RG 17(a)'s 2 January Holy Name fallback, a genuine feast sitting outside Temporal_ef.named for an architectural reason), fixed by naming that date explicitly rather than narrowing the span. Proved the test has teeth by mutation: forcing the ferial builder's default subject to Lord reddened it (and 24 other tests, expected collateral from an intentionally broad mutation) before being reverted. No production logic changed -- only comments in rubrics_ef.ml. day, readings and rubrics output confirmed byte-identical against the pre-fix binary for four spot years across the domain. Also corrects a stale test-case count in a prior task report (18 in the array, not the 19 recorded there); the array now genuinely holds 19.
* feat(ef): the Creed, RG 475-476Lukasz Kasprzak2026-08-2217-29/+612
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Whether the Creed is said at Mass. New Rubrics_ef.creed, reached through a new Rite.t.creed field and a new Liturgical_day.t.creed bool (false, not an option, for a rite that has not implemented the rule); colitur rubrics gains a fourth TAB-separated column. 475(a) reads the TEMPORAL cycle's own weekday, not the observed day: a Sunday impeded by a Feast of the Lord (RG 16a) still says the Creed. 475(d)'s three octaves (Nativity, Easter, Pentecost) are pure date/ Easter-offset windows, checked first, since a saint's feast winning the day inside one of them still says the Creed ("etiam in festis occurrentibus") -- St Stephen, 26 December, is the live witness. RG 23 classifies Ash Wednesday and every feria of Holy Week (including the Sacred Triduum) as feriae, not festa, however high their rank, so 475(b)/(c)'s "in festis" never reaches them; this single check subsumes 476(a)'s own naming of the Chrism/Lord's-Supper Mass and the Easter Vigil. RG 28-34 vigils are a third liturgical-day category, also excluded from "in festis" regardless of rank -- reuses the already- exported Precedence_ef.is_vigil rather than a new list. 475(e)'s Apostle/Evangelist natalicia list (creed_apostle_slugs) was derived by grepping data/ef/sanctoral.sexp directly and checking each candidate's own date against whether it is that saint's dies natalis -- not copied from any list supplied with the task. The Conversion of St Paul and the 30 June Commemoration of St Paul are excluded (neither is a natalicium); the Chair of St Peter and St Barnabas are included only because the clause names them explicitly, which is exactly why it has to: neither is a natalicium either. 475(c)'s BVM half reuses Precedence_ef.marian_slugs (newly exported) rather than the subject field alone: checked against the shipped data, almost every Marian sanctoral entry ships subject=Saint, not Bvm. man/colitur.1's rubrics section is updated to match the new column; test/cli.t repinned via dune promote for the same reason. Verified day/readings output byte-identical to v0.10.1 across the whole 1583-9999 domain (both binaries' concatenated day+readings output, 6,148,492 lines each, zero diff). Domain-wide: 882,996 days say the Creed, 2,191,250 do not; every one of the domain's 439,178 Sundays says it, zero exceptions.
* feat(cli): colitur rubrics prints the day's Mass formularyLukasz Kasprzak2026-08-223-18/+248
| | | | | | | | | | | | | | | | | | | | | | Separate from day for the reason readings is: a formulary name contains spaces and day's row is space-separated with a variable-length tail. Modelled on readings' own structure (year parsing, --overlay plumbing, error handling): --overlay is accepted, since an overlay can change which celebration is observed and hence which Mass is said. --lang, --raw and --sigla-* are refused rather than silently ignored -- this row resolves no display name and no citation for any of them to affect. Output is TAB-separated (date, formulary slug, source), not space or " | " like the two existing reports: a resolved formulary NAME is a column a later task may add, and it can carry both spaces and punctuation a citation never does, ruling out either separator already in use. day and readings verified byte-identical (sha256) against a v0.10.1 build across 1583/1900/2026/2038/9999. Man page documents rubrics beside readings and renders through groff with no errors.
* docs(ef): retract the false LMS Ordo witness claim for Mass_formulary.VotiveLukasz Kasprzak2026-08-223-23/+61
| | | | | | | | | | | | | | | | | | | | | | | | | Fix round 1's own comment claimed the Latin Mass Society Ordo witnesses the Votive tag directly, reading its "V Mass of BVM" as an abbreviation for "votive". That was wrong: the Ordo's I-V are roman numerals naming WHICH of the Missal's five seasonal "Missae de sancta Maria in sabbato" (RG 309(a)'s own "iuxta temporum diversitatem") is said on a given Saturday, not a marker of the Mass's kind. All five numerals occur through the Ordo (I x1, II x4, III x3, IV x3, V x8); the false reading came from "V Mass of BVM" also being a substring of "IV Mass of BVM". Votive itself is unaffected and stays correct on RG 431(e) alone, quoted verbatim in its place: "e) in Missis votivis IV classis de Angelis, quocumque die, et de B. Maria Virg. quae in sabbato celebrantur" -- votive Masses of the IV class, of the BVM, celebrated on Saturday. The retraction is recorded, not softened, in lectionary_ef.ml/.mli and the test pin's own comment, together with what the Ordo's numeral actually means so the mistake is not repeated, and the consequence it leaves behind: because the numeral names WHICH seasonal Mass is said, a future comparison against the Ordo (Task 6) can validate bvm_saturday_citations' own season-keyed selection, not merely that some BVM Mass was chosen. Comments and docs only -- no behaviour change. Both suites re-run green and colitur day re-confirmed byte-identical.
* fix(kernel,ef): a votive Mass is not the day's own -- add Mass_formulary.VotiveLukasz Kasprzak2026-08-216-26/+69
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Mass_formulary.source was missing a case for a Mass said IN PLACE of the day's own office's Mass while that office is itself kept unchanged -- RG 309(a) and RG 431(e) both classify the RG 78 Saturday Mass of Our Lady this way, in the Missal's own words, as a "Missa votiva IV classis... de B. Maria Virg.", not as the day's own office's Mass. The Latin Mass Society Ordo (docs/research/ordo/lms-ordo-2024-2025.pdf) witnesses it directly, printing that day as "V Mass of BVM". The BVM-Saturday branch in Lectionary_ef.readings was tagged Own_slug for lack of a better constructor when Task 2 landed, flagged there as a judgement call rather than a specified answer. That call was wrong: left as Own_slug, a future comparison against the LMS Ordo (a later task in this plan) would read every BVM Saturday as a manufactured divergence between colitur's "own" Mass and the Ordo's votive one. Retagged to Votive; said is unchanged (still the day's own, reused ferial, temporal slug) since the office itself is unaffected, only the Mass said for it. Added the new constructor's source_to_string case ("votive") and its own test row, and pinned the branch itself in test_lectionary_ef.ml's formulary cases at 1 August 2026, verified directly against the resolver rather than assumed. Both this session's own drifted pins that the earlier commit inherited from the task brief are unaffected by this change, and both are re-confirmed independently correct in this round: dune test and the exhaustive sweep are green, and colitur day stays byte-identical against the pre-fix-round binary.
* feat(kernel,ef): the lectionary reports which Mass it saidLukasz Kasprzak2026-08-2112-46/+281
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Rite.readings now returns (Mass_formulary.t option * Citation.t list) instead of a bare citation list, and Liturgical_day.t carries the result as a new formulary field. Validate holds a rite that resolves a formulary at all to resolving one on every day, the same discipline it already applies to citations; the EF lectionary chain resolves Some on every day of every year 1583-9999, confirmed by a direct sweep over 2005-2050 as well as through Validate itself. Plan Tasks 2 and 3 are merged into this one commit on the coordinator's own instruction: Rite.readings' signature and the field that consumes it are one atomic edit, and the intermediate state does not compile on its own. Each of the four lectionary steps now builds its own Mass_formulary.t at the point it decides, not by re-deriving it afterwards from the citations it returns: step 1 tags Proper with the observed slug, step 2 tags Own_slug with the day's own temporal slug, step 3 tags Preceding_sunday with the resumed Sunday's temporal slug, and step 4 tags Common with the Common's own id -- Commons.find now returns that id alongside its citations rather than discarding it, since it is only ever in scope at the point the assignment is looked up. The RG 309(a) Saturday votive Mass of Our Lady, which answers between steps 4 and 2 rather than as one of the four numbered steps, is tagged Own_slug too: Mass_formulary.source has no dedicated constructor for it, and its own guard only ever fires when the observed celebration already is the day's own (reused ferial) temporal slug, which is exactly what Own_slug documents. Recorded as a judgement call in the task report, not a specified answer. test/cli.t's `emit --format sexp` line count is repinned (8472 to 8881): that command serializes Liturgical_day.t whole, so the new field grows its output. `colitur day` itself is untouched -- verified byte-identical against the pre-change binary across 1583, 1900, 2026, 2038 and 9999.
* feat(kernel): a type for which Mass a day saysLukasz Kasprzak2026-08-214-1/+65
| | | | | | | The lectionary's four-step chain already decides whether a day says its own proper, its own slug's entry, the preceding Sunday's Mass or a Common, and then discards that decision once the citations are out. An ordo needs to print it.
* release: v0.10.1v0.10.1Lukasz Kasprzak2026-08-213-3/+3
|
* docs: CHANGELOG entry for 0.10.1Lukasz Kasprzak2026-08-211-0/+3
|
* docs(C39): day-level confirmation from the LMS OrdoLukasz Kasprzak2026-08-212-8/+20
| | | | | | | | | | | | | | | | | | | | | | | | The Latin Mass Society Ordo 2024-2025 for England and Wales (v2.21, compiled by Peter Day-Milne) gives 9 August 2025 as "OUR LADY on SATURDAY IV Cl W / V Mass of BVM / Commem of St Romanus M at Low Mass" -- no vigil -- and 10 August as the 9th Sunday after Pentecost with St Laurence commemorated. That is colitur's output on both days, field for field. It is discriminating where the Ordo already in docs/research was not. This edition lists six vigils as the day's office across its year, including the Assumption's on 14 August 2025, and its footnotes discuss arranging a diocesan feast around the Vigil of St Laurence in other years. Its absence on 9 August 2025 is an application of the rule, not a data gap. The extraordinaryform.org Ordo omits that vigil on every date it covers, including 2027-08-09 where the clause says to keep it, which is why it witnesses nothing here. Also confirms the Vigil of the Assumption as II Cl violet, the RG 128 colour this project corrected from the bootstrap's White. The clause's other shape -- feast transferred away, 2022/2033/2038/2044 -- remains unwitnessed; no accessible Ordo covers those years. Stated in the note rather than left implied.
* merge: RG 33's third omission triggerLukasz Kasprzak2026-08-2116-26/+494
|\ | | | | | | | | | | | | | | | | | | | | The last unbuilt clause in the EF module. A II/III-class vigil is omitted entirely when the feast it precedes fails to keep its own day -- 1 744 days across 1583-9999, three verified shapes, zero unexplained. Rule and vigil inventory independently confirmed by O'Connell 1964, outside the Divinum Officium -> missalemeum -> lectio lineage. The 10 affected days in 2005-2050 are allow-listed as C39; no external day-level witness exists, which the note states rather than glosses.
| * docs(C39): O'Connell 1964 confirms the rule and the vigil inventoryLukasz Kasprzak2026-08-211-1/+7
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Replaces the withdrawn Ordo claim with a real one of a different kind. O'Connell, The Celebration of Mass (4th ed., 1964) -- a rubrics commentary contemporary with the 1960 Code, outside the Divinum Officium -> missalemeum -> lectio lineage, already cited here for RG 128 -- states all three of RG 33's triggers in English, the third included, verbatim in substance with the Latin. The same passage confirms vigil_feast_table's membership independently: Christmas Eve and Pentecost I class and so outside the rule; Ascension, Assumption, John the Baptist and Peter & Paul II class; St Lawrence the sole III-class vigil. Five entries, the same five, same class split. It confirms the RULE and the ENTITY LIST, not the 10 days -- O'Connell publishes no day-by-day calendar. The distinction is stated in the note rather than blurred, since blurring it is what the withdrawn Ordo paragraph did.
| * docs(C39): withdraw the Ordo corroboration claimLukasz Kasprzak2026-08-212-5/+11
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | C39 and the 2025 golden pin both claimed the published Ordo independently confirms RG 33's third trigger, because it omits St Lawrence's vigil on 2025-08-09 where lectio and missalemeum keep it. It does not. The Ordo omits that vigil on 2027-08-09 as well, where the clause says to keep it (10 August is a Tuesday, the feast holds its day), and 2026-08-09 is a Sunday where the rule's first trigger omits it anyway. The 2024 date has no row. So the Ordo never shows St Lawrence's vigil on any date it covers, and the 2025 agreement is a blanket gap coinciding with the right answer, not evidence. The rule itself is unaffected: it rests on RG 33's text, which admits no latitude, and on lectio not evaluating the clause at all -- "RG 33" appears nowhere in its production code, and the third trigger cannot emerge from per-day rank comparison because it turns on what is observed the following day. The divergence is architectural, not a disagreement about the rubric. Recorded rather than deleted: the withdrawn claim is why this entry was first written up as unusually well corroborated.
| * docs(C39): correct the shape split, 7/3 -> 6/4Lukasz Kasprzak2026-08-211-1/+1
| | | | | | | | | | | | | | The note apportioned the entry's ten rows as seven commemoration-reduced and three transferred, counting 2038-06-23 in both halves to make the arithmetic work. It is six and four; the row count itself was always right, and is pinned by the comparator.
| * chore: gitignore .superpowers working artifactsLukasz Kasprzak2026-08-211-0/+4
| | | | | | | | | | | | Agent research reports, SDD ledgers and review packages, same local-only reasoning as docs/. Untracked but not ignored until now, so they showed up in every git status.
| * feat(ef): implement RG 33's third omission triggerLukasz Kasprzak2026-08-2115-26/+478
|/ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | RG 33 omits a II/III-class vigil in three cases: it falls on a Sunday, it falls on a I-class feast, "vel si festum cui praemittitur in alium diem transferri aut ad commemorationem reduci contingat". Only the first two were built; the third was recorded in precedence_ef.ml as unimplemented on the grounds that no witness existed in the shipped data. That reasoning was wrong, and the rule fires on 1 744 days across 1583-9999. Both halves of the clause reduce to one observable question -- is the feast the OBSERVED office on the following day (RG 34 puts it there) -- so the kernel asks it once per candidate, after place_transfers has settled the year. No fixed point is needed: a vigil is a candidate only on its own day, never on its feast's, so suppressing it cannot change what the next day observes. Precedence.rules gains vigil_feast, which returns the slug of the feast a vigil precedes; the kernel cannot infer that itself, because only two of the five vigil/feast pairs share a slug stem. Blast radius, measured pre-change binary vs HEAD over the whole domain and classified: 1 744 days, three shapes, zero unexplained. 1 199 are the feast reduced to a commemoration (10 August on a Sunday, St Lawrence); 478 and 67 are the feast transferred under RG 96 after the Sacred Heart or Corpus Christi takes its day. The Assumption's and the Ascension's vigils never qualify -- their I-class feasts always keep their own day. Independently witnessed, which is unusual here. The published Ordo -- the only witness outside the Divinum Officium -> missalemeum -> lectio lineage -- omits St Lawrence's vigil on 2025-08-09, agreeing with colitur against both engines. That date had been read earlier as an Ordo gap; the Ordo was right, and correcting the misreading is what surfaced this clause. On 2027-08-09 the feast does keep its day and the Ordo omits a vigil colitur correctly keeps, which is a genuine Ordo gap. Allow-lists: C39 (lectio, 10 rows) and a 2038 oracle class citing the register, the 2026-2027 window having no instance. The golden pin asserting St Lawrence's vigil is violet moved 2025 -> 2027; its own comment had reasoned about the vigil's weekday and missed that RG 33 also looks at the feast's. Two new pins cover both shapes of the clause. The vigil/feast table is built with Slug.of_string_exn: mutation testing showed that of_string plus Result.to_option turns a typo into None, which this hook's contract reads as "not a vigil", switching the rule off in silence. Two tests assert the table against the shipped data in both directions.
* docs(config): document the flavour keyLukasz Kasprzak2026-08-211-0/+40
| | | | | | | | | | | | colitur.1 documented the flag and colitur-config(5) documented every [defaults] key except this one, which the previous commit added. Says what a flavour IS rather than only listing the values: the set of characters that must be neutered so a value cannot corrupt its target. A semicolon is ordinary text in LaTeX and a field separator in iCalendar; an unescaped & or _ does not look wrong in LaTeX, it aborts the build. Also why unset means INFER rather than 'no escaping', and why there is no silent fallback.
* release: v0.10.0v0.10.0Lukasz Kasprzak2026-08-213-3/+3
|
* feat(config): flavour is settable, and an INI overlay is diagnosedLukasz Kasprzak2026-08-216-5/+77
| | | | | | | | | | | The escaping flavour was the one rendering setting with no config key, so a template whose extension says nothing needed the flag every run. Unset still means infer, which config --show reports as (infer) rather than (none). Backfills the 0.8.0 and 0.9.0 changelog entries. Both were tagged by hand rather than through 'make release', which is precisely the guard that would have refused a release with no changelog line.
* fix(cli): diagnose an INI overlay passed to --overlayLukasz Kasprzak2026-08-212-0/+45
| | | | | | | | | | | | | | The INI overlay form is a SOURCE format that 'colitur convert' turns into the S-expression one. Handing the INI to --overlay instead failed deep in the sexp reader with 'more than one S-expression in file', which names neither the cause nor the cure -- and the shipped examples under data/ef/examples/ are exactly the files someone would try that with. The detector reads past the comment header to the first real line and looks for a [section]. Its first version capped that scan at 40 lines and poland.ini's own header is 43, so it silently never fired on the very file it was written for; the cap now bounds only how much of a binary gets read, and is not a guess at header length.
* docs: correct a stale READMELukasz Kasprzak2026-08-211-10/+44
| | | | | | | | | | | | | | | | | | | | | Three claims were false: The download paragraph said older tags are not offered for download. They are again -- per-tag snapshots were re-enabled, and only the summary page's tag list is trimmed. A stable colitur-latest.tar.gz still exists for scripts that should not know a version. 'Starting with the Roman EF (1962) and OF forms' read as though both existed. lib/rites/ holds rite_ef and nothing else; the OF is the next module. The tagline claimed multiple rites for the same reason. Suite timings were from a much smaller suite: the fast run is ~8s over 568 tests, not ~3s, and the exhaustive sweep ~80s. Adds a Status section with the real numbers, the five validation layers and -- more usefully -- what they cannot see, since the differential and the oracle share one data lineage. Notes that chants are deliberately absent, and points at the colitur-x viewer.
* build: verify the upload instead of assuming itLukasz Kasprzak2026-08-211-5/+21
| | | | | | | | | | | | release-tarball chained its steps with ';', so a failed scp or ssh still reached the final echo and it announced a publication that had not happened -- the file was never in the docroot. Every step is now chained with && and the result is checked over HTTP before anything is claimed: the URL must respond and the bytes must untar. Also passes -o ClearAllForwardings=yes, since the i2p port forwards in the ssh config fail when another session holds them and their noise buries real errors.
* build: publish the latest tag, not strictly HEADLukasz Kasprzak2026-08-211-2/+4
| | | | | | | Requiring HEAD to be exactly a tag made the target unusable: the commit that adds the download link to the README necessarily comes after the tag it links to. It now archives the most recent tag and says so when HEAD is ahead of it.
* build: publish one current tarball instead of per-tag snapshotsLukasz Kasprzak2026-08-212-2/+28
| | | | | | | | | | cgit offers snapshots for every tag or for none, so colitur.git now has cgit.snapshots=0 and 'make release-tarball' uploads the tagged archive to a stable URL. Ten tags had meant twenty download links, nineteen of them stale. The target refuses to run from an untagged or dirty tree, so what is published always matches a tag.
* chore: v0.9.0v0.9.0Lukasz Kasprzak2026-08-203-3/+3
| | | | | | | | | | | | Robustness and hackability pass over the shipped program. The citation parser is strict about numbers, four pairs of books no longer share a full title, shipped styles re-parse their own output, and overlay errors are written for the person editing the file rather than for a compiler author. colitur-templates(5) already refers to "before colitur 0.9" for the month-key change, so the version moves with it.
* Merge branch 'robustness-and-hackability'Lukasz Kasprzak2026-08-2016-95/+598
|\ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | An audit of the shipped program, plus the fixes it found. The citation parser accepted OCaml integer-literal syntax, so a typo like 'Luke 1_1:5' silently became a different chapter. Four pairs of different books shared a full title -- 1 and 2 Corinthians both rendered 'Epistola ad Corinthios' -- leaving 108 citations in 2027 alone that a reader could not resolve to a book. Spec section 8.5 is now delivered rather than recorded: shipped styles re-parse their own output. Overlay errors no longer name OCaml source files at the reader. Also: the new-overlay scaffold shows citations at the right nesting level, config --show validates before printing, error messages no longer echo whole file lines, and a month answers to both spellings of its own name.
| * fix(render): a month answers to both spellings of its own nameLukasz Kasprzak2026-08-203-5/+62
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | A week object carries month_name/month_num/month_abbr, because at that level a bare `name` would be ambiguous. An author who learned those names inside {{#weeks}} reaches for them one level up inside {{#months}} too -- where they resolved to nothing, and an unknown key renders as the empty string by design, so the result was a silently blank month heading rather than any error. Found by writing a template from scratch rather than copying a shipped one; every shipped template sidesteps it, which is why nothing caught it. The aliases are additive, so no existing template or golden changes. colitur-templates(5) now documents both spellings and says why they exist.
| * fix: audit findings — parser strictness, name ambiguity, and errorsLukasz Kasprzak2026-08-2013-90/+536
|/ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Found by auditing the shipped program rather than the diff. The parser accepted OCaml integer-literal syntax, so "Luke 1_1:5" read as chapter ELEVEN and "+5" as 5 -- a typo silently becoming a different chapter, reachable through any user overlay. Numbers are now plain digits and positive, and a descending range is rejected: 1:20-10 is always a transcription error. No shipped citation changed. FOUR PAIRS OF DIFFERENT BOOKS SHARED A FULL TITLE. 1 and 2 Corinthians both rendered "Epistola ad Corinthios", as did Thessalonians, Timothy and Peter -- 108 citations in 2027 alone that a reader cannot resolve to a book. This is the Kings defect fixed earlier and not generalised. The titles now carry their volume numeral, marked CONSTRUCTED, and a test asserts no two books share a name -- while allowing the case where two ids ARE the same book under different numbering, which a tradition relates. Spec section 8.5 is now delivered rather than merely recorded. Shipped styles did not re-parse their own output: 32 of 52 Latin abbreviations and 49 of 52 full titles failed, so a citation copied from colitur's own output into an overlay was passed through untouched and printed in the wrong language, silently. Every shipped name is registered as a spelling and split_book learned multi-word titles by longest-token match. Now 0 of 52 fail beyond the same-book aliases. Overlay errors were written for a compiler author: they named an OCaml source file the reader does not have and buried the useful token. The existing five-path rewriter is replaced by a generic one, applied to every load path rather than one, so "rank: is not one of the allowed values (at Class9)" replaces the raw Of_sexp_error dump. Also: the new-overlay scaffold documented citations and layer without showing them, and its comment implied the wrong nesting -- the single easiest thing to get wrong; error messages echoed whole file lines, copying an unrelated file's contents into stderr when a flag pointed at one; and config --show validated partway down its table, exiting 2 after writing five rows to stdout.
* chore: v0.8.0v0.8.0Lukasz Kasprzak2026-08-202-2/+2
| | | | | | | | | | Citations become configurable: parsed into structure and re-rendered, so book names, abbreviations, punctuation style and numbering tradition are files a user edits rather than strings frozen in the data. New lib/citation (Book, Parse, Render, Sigla); [bible] and [sigla] sections in language files; lang/traditions.ini; three config keys and CLI flags. --raw emits every citation byte-for-byte as stored.
* Merge branch 'citations-and-sigla'Lukasz Kasprzak2026-08-2046-1679/+4600
|\ | | | | | | | | | | | | | | | | | | | | | | Citations are parsed into structure and re-rendered, so book names, abbreviations, punctuation style and numbering tradition become files a user edits rather than strings frozen in the data. New lib/citation (Book, Parse, Render, Sigla); [bible] and [sigla] sections in language files; lang/traditions.ini for numbering; three config keys and CLI flags. --raw emits every citation byte-for-byte as stored, bypassing the whole pipeline, so output stays diffable against lectio and the raw view does not depend on the parser being correct.
| * fix(citation): close the final review's blocking findingsLukasz Kasprzak2026-08-207-18/+94
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The branch was RED and reported green. `dune test` exited 1: test/cli.t pinned the pre-fix output `kings_1 19:3-8`, which the previous commit had already fixed to `1 Reg 19:3-8`. The gate command piped dune through `tail`, so it reported tail's exit status, and cram prints its diff BEFORE the alcotest summary, so the two lines shown were the passing ones. Verify with `dune test; echo $?`, never through a pipe. A style file's own `book` key was unreachable. sigla_book resolved against a hardcoded "abbr" and the result was applied unconditionally, so the documented `[sigla] book = full` could never win. Render gains book_string, and the style's own value is now the default that a flag or config overrides. The unit test pinned style_of_fields correctly while the wiring defeated it. `lang --check` filtered the reference set to the celebration prefix, so a file with no [bible] section at all reported a clean bill of health -- contradicting both the reason the keys change was made and lang.ml's own comment. It now reports missing book names too. The token test missed a FOURTH citation-bearing file: adjustments.sexp writes citations as `Set_citation`, not `(reference ...)`. Its 16 citations all parse, so nothing was broken, but nothing was checking. The first attempt at this fix read the file and extracted NOTHING -- the marker stopped before the opening quote, so every payload was the part label -- which is recorded in the code rather than left as a trap. Also: colitur-config(5) claimed a trailing period the data does not carry, and two la.ini scan quotes silently corrected OCR damage ("Ionae 3, I - I O", "Epistolse") while presenting themselves as verbatim. Both are now marked as corrections.
| * fix(lang): stop internal book ids reaching output, and finish the docsLukasz Kasprzak2026-08-204-17/+132
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Three defects, all found by running the documented examples rather than by reading code. All seven modern-numbering targets carried their own internal id as their Latin name (kings_1.full = kings_1), so --sigla-tradition modern printed "kings_1 19:3-8" -- leaking a key book.mli states is never shown to a reader. They now carry Vulgate-derived Latin names marked CONSTRUCTED: the 1962 Missal uses Vulgate numbering throughout, so it can contain no incipit for a book that exists only under a later convention. In Latin the modern tradition therefore only moves Kings and Esdras; Osee, Ionas, Ecclesiasticus and the Apocalypse keep their names either way, since modern numbering is a vernacular convention. The coverage test could not catch this. It compares a name against its KEY ("kings_1.full"), and "kings_1" differs from that, so the row passed. A second check now asserts no name IS its own id. The two are complementary: one catches a missing row, the other a row present but filled with the wrong thing. Mutation-proved. --help still claimed sigla settings were resolved "but not yet rendered", stale since the render wiring landed. colitur-config.5 documented none of the three settings even though config --show prints them.
| * docs(citation): sigla flags, book naming and traditionsLukasz Kasprzak2026-08-202-5/+437
| | | | | | | | | | Includes that --raw emits citations verbatim, which is the behaviour someone diffing against lectio depends on.
| * fix(lang): disambiguate the two Books of Kings in full modeLukasz Kasprzak2026-08-201-6/+12
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Both kings_3 and kings_4 carried the full title "Liber Regum", so --sigla-book full rendered 3 Kings and 4 Kings identically and a reader could not tell which book a citation named. Seven citations in 2027 alone. The cause is real and now recorded rather than papered over: this Missal's incipit "Lectio libri Regum." is UNDIFFERENTIATED -- that exact unqualified phrase precedes both a "3 Reg." and a "4 Reg." locator in the same book -- so it is not a valid full title for either volume on its own. Only the numbered locator distinguishes them, and both "3 Reg" and "4 Reg" are independently attested in the scans. The full titles are therefore marked CONSTRUCTED, a third category beside sourced and UNSOURCED: both halves are transcribed, their combination is not. The numeral is written in Roman to match this project's own output convention ("Feria IV", "Hebdomada I"). Abbreviated and --raw output are unchanged.
| * feat(lang): Latin and English book names, and the shipped sigla stylesLukasz Kasprzak2026-08-2015-175/+730
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | la.ini and en.ini both gain [sigla] (the current Vulgate/Latin punctuation convention, byte-identical to Render.default_style) and [bible] (a .full and .abbr row for every id in Book.all -- 45 cited ids plus the 7 tradition targets, 52 total). Shipping [sigla] changes no output, asserted by the full test run. Shipping [bible] does change rendered book names, which is the point. la.ini's titles are sourced from docs/research/scan1.txt/scan2.txt (the 1962 Missal scans), each row citing the line its incipit pattern was read from. Six pairs (kings_3/4, corinthians_1/2, thessalonians_1/2, timothy_1/2, peter_1/2) share one Missal incipit and differ only in the sourced .abbr, matching what the primary text itself does. Three ids (proverbs, song_of_songs, ecclesiasticus) and the seven tradition targets are marked UNSOURCED and fall back to the data's own spelling, per this project's central rule against inventing a Latin title. Re-running the task's own sourcing note against the scans, counting every hit rather than eyeballing a frequency-sorted list, found nine of its NOT-sourced verdicts were undercounted (a single clean hit, buried under higher-frequency matches): Galatians, Colossians, both Thessalonians, both Peter, Malachi, Numbers, Jonas, Osee and Esdras all have a clean incipit in the scans and are sourced here. The note's other three verdicts stand, confirmed independently. Ecclesiasticus is not simply unfound: this Missal reuses Wisdom's own Lectio libri Sapientiae incipit for Ecclesiasticus readings too (both were anciently classed as one Sapiential group), so using it for Ecclesiasticus would misidentify the book, not merely abbreviate it -- recorded in that row's own comment. en.ini's [bible] is filled in full, not left partial the way [celebration] is: without it, a Vulgate-numbered id would fall through the [meta] fallback chain to la.ini's Latin name, not merely a less complete English one. Traditional Douay-Rheims names for the Vulgate ids (3 Kings, Osee, Ecclesiasticus, Isaias, Apocalypse), modern names for the seven tradition targets, since --sigla-tradition modern is the reader asking for modern numbering. test_lang_coverage.ml gains test_every_book_named, asserting every Book.all id has both forms in la.ini -- the check that catches a forgotten tradition target, since nothing else in the suite ever names them. Shipping real book names changes 11 golden template renders (Render/golden) and several test/cli.t examples that used to demonstrate the pre-Task-10 default-spelling fallback; both are updated to the new, correct output, each line checked against a fresh render before promoting.
| * feat(render): citations render through Sigla at both call sitesLukasz Kasprzak2026-08-2018-1558/+1804
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | View.citation_ref and readings' part_ref are the only two places a citation reaches output; every emitter goes through View. --raw passes Sigla.verbatim rather than a style built over Lang.raw: an identity name table would still reformat punctuation and renumber. Loosen reject_sigla_for: --sigla-style/--sigla-book/--sigla-tradition now actually render on readings/table/render/emit/publish (each builds its own Sigla.t via the new load_sigla), and stay refused only on commands that render no citation (day, easter, temporal, ...). names_of degrades a Lang.bible miss to Book.default_spelling rather than printing the lookup key itself ("luke.abbr"); pinned in cli.t against a language file with no [bible] section at all, independent of la.ini/en.ini's own eventual [bible] section. Regenerate the 11 golden templates for 2027: every changed line is one canonical citation replacing a stored variant, verified against the full readings 2027 diff (raw vs default) cell by cell -- 86 distinct (raw, rendered) pairs account for the entire diff across all 11 files, with zero unexplained residue. Two families: the seven duplicate book spellings collapsing onto one canonical form (Isa./Isa, 3 Kgs./3 Kings, ...), and citations reconstructed from parsed structure dropping stray punctuation the parser already treats as noise (a trailing period or semicolon, a comma chapter/verse separator, an elided inherited chapter) -- each of the latter already named and tested in test_citation.ml's parse_suite before this task.
| * feat(config): sigla_style, sigla_book and sigla_traditionLukasz Kasprzak2026-08-205-34/+284
| | | | | | | | | | | | | | | | | | Same flag > config > default precedence as --lang, and each reported by config --show with its source, so an override is visible rather than mysterious. A language file's [sigla] section IS a style; a config key SELECTS one and may override settings within it -- the two are not a duplicate setting.
| * fix(citation): complete the modern tradition, and test the shipped fileLukasz Kasprzak2026-08-202-4/+41
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Apocalypse -> Revelation was missing from [modern] while `revelation` was already a declared tradition target, so the target existed with nothing mapping onto it. Same Vulgate-to-modern shape as Ecclesiasticus -> Sirach. The injectivity test used to hand-copy the mapping table, which asserted things about the copy and nothing about the artifact: a mapping added to traditions.ini and forgotten in the test would have passed. It now reads lang/traditions.ini and additionally asserts that [vulgate] is empty -- an entry there would silently renumber the DEFAULT, the one thing this design promises never happens -- and that no declared target is left unreachable. Mutation-proved three ways: declaring an unused target, deleting a mapping, and adding an entry to [vulgate] each redden it, the first two naming the offending book.
| * feat(lang): traditions.ini, and its install ruleLukasz Kasprzak2026-08-206-6/+126
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Which book a reference denotes does not vary by language, so it lives in its own file rather than in la.ini beside [weekday]. Six mappings, not four: Osee/Jonas are the same question as 3 Kings in transliteration form. The install rule is added in the same commit deliberately -- lang/ once shipped without one, so the feature worked from the source tree and was broken once installed. bin/dune gains colitur_citation as a linked library, needed for the new loader; lang_list's directory scan now excludes traditions.ini, which is not a language file and does not parse as one.
| * feat(lang): read [bible] and [sigla] sectionsLukasz Kasprzak2026-08-203-4/+95
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Lang reads a hardcoded list of section names and ignores anything else silently, so without this a [bible] section would appear to work and do nothing -- confirmed directly with a scratch executable: before this change, of_string on text containing a [bible] section parsed without error and Lang.keys came back empty, no trace of the section anywhere. Lang.bible keeps the total-lookup contract every other lookup in this module has: a miss returns the key itself, never the empty string. Lang.sigla_fields returns the [sigla] section's raw fields, values trimmed but still quoted, for Render.style_of_fields to unquote. [bible] joins the keys reference set so lang --check reports missing book names; [sigla] deliberately does not, being settings with working defaults rather than translatable names -- adding it would make --check demand five settings from every language file.
| * feat(citation): the Sigla facade, total by constructionLukasz Kasprzak2026-08-205-24/+144
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | format never raises and returns an unparseable citation unchanged, so a gap degrades to today's behaviour rather than to a crash. The coverage test asserts separately that no shipped citation takes that path. verbatim is what --raw uses: an identity name table would still reformat punctuation and renumber, which would break byte-exact diffing against lectio. The existing citation-coverage walk (1970-2070) now also drives a round-trip check in the same pass: parse -> render -> parse must reach the same structure, using Book.default_spelling for names (Book.to_string returns the internal id, which is not a registered token and cannot be read back). Proved with a mutation: changing the default style's part_sep to a separator the parser does not accept reddened the round-trip case on 31 multi-part citations and nothing else; reverted.
| * feat(citation): default_spelling, and close two test gapsLukasz Kasprzak2026-08-203-0/+72
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Book.default_spelling returns the first registered spelling for an id. It is the fallback display name, and it exists because the alternative is worse: a language file's [bible] lookup is total and returns THE KEY on a miss, so a book with no entry would render as "luke.abbr 5:12-14". Falling back to the data's own spelling makes it render as "Luke 5:12-14" instead -- what colitur printed before this feature existed. The degraded case is the old behaviour, the same principle Lang states for its own key-returning misses. Two test gaps closed, both found by mutation rather than by reading: Parse's split_book scans a leading ordinal digit over '1'..'4', and no case in the suite used an ordinal above 1. Narrowing the range to '1'..'3' passed every test while seven real citations depend on it ("3 Kings 17:8-16", "4 Kings 5:1-15"). Parse-layer cases added; the first attempt at this test asserted through Book.of_token, which is a table lookup and never reaches split_book at all. default_spelling is asserted to round-trip: every cited id's fallback spelling must itself resolve back to that id, or Render and Parse disagree the moment a book goes unnamed.
| * feat(citation): render a parsed citation in a configurable styleLukasz Kasprzak2026-08-204-0/+267
| | | | | | | | | | | | | | | | | | A style is a set of format strings, so punctuation convention is data. Values are unquoted here rather than in Overlay_ini: that parser trims every value and is shared with overlays and [defaults], so teaching it about quotes would change behaviour this feature has no business changing.
| * test(citation): assert every emitted citation parsesLukasz Kasprzak2026-08-202-0/+44
| | | | | | | | | | | | | | | | | | Walks 1970-2070 and parses every citation the engine emits. Nothing validated citation spellings before this, which is why seven books carried two spellings unnoticed. Mutation-proved: removing the 'Isa.' spelling from the book table reddens this test naming that citation.
| * feat(citation): parse citations into structureLukasz Kasprzak2026-08-204-0/+180
| | | | | | | | | | | | | | | | | | The parsed form is a book and a LIST of chapter-parts: the data cites across chapters and lists disjoint verse ranges within one. Two rules the shipped data forces and that are not obvious: a semicolon-separated part may inherit the previous chapter rather than restate it, and a chapter may be separated from its verses by a comma.
| * fix(citation): survey all three citation-bearing files, not just the lectionaryLukasz Kasprzak2026-08-203-20/+161
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The book table was built against data/ef/lectionary.sexp alone. That undercounts: sanctoral.sexp carries more citations than the lectionary and, together with commons.sexp, cites 21 book tokens the table was missing entirely (62 distinct tokens across all three files, not 42), several of them common (2 Tim appears 8+ times). Add the missing spellings to existing ids (2 Cor, Col., Exod, Ezek, James, Sir, Eccli) and ten new ids for books not cited before (1-2 Timothy, 2 Peter, Apocalypse, Judith, Malachi, Proverbs, Song of Songs, Tobit, Wisdom). Sir and Rev are modern spellings sitting inside Vulgate data, so both resolve to their Vulgate ids (ecclesiasticus, apocalypse) rather than to the sirach/revelation tradition targets -- mapping them to a second id would double-map the same book. Add a duplicate-spelling invariant test (List.assoc_opt would silently prefer the first match on a collision) and a test that re-derives the token set from all three data files at test time and asserts every token resolves, rather than trusting a survey performed once by hand.
| * feat(citation): canonical book ids and tradition mappingLukasz Kasprzak2026-08-206-1/+170
|/ | | | | | | | | Seven books arrive in two spellings, inherited from lectio's ini and ultimately from Divinum Officium. Collapse them onto one id here rather than editing generated data. Naming and renumbering are kept apart: a tradition decides which book an id denotes, a language file decides what it is called.