# colitur — working context for Claude Read this first, then skim the two authoritative docs it points to. This file orients a fresh session; the specs hold the exhaustive detail. ## What colitur is *computus liturgicus* + Latin `colitur` ("He is worshipped"). A **safe, highly-tested, deterministic OCaml engine** that computes and validates **liturgical calendars** for multiple rites and emits universal, template-driven output. It computes the **day identity** (season, week, cycle, observed celebration with rank/colour/flags, commemorations, transfers) and the day's **reading citations** (references like `Jn 3:16`, never Bible text), correct all the way to year **9999**. Starts with the Roman **EF (1962)** and **OF** forms; the architecture generalizes to any deterministic rite (Byzantine, Ambrosian, pre-Trent) as future modules. **Sibling projects** (same author, `~/git/projects/`): **lectio** (Go; the shipped OF+EF readings engine, 0-error vs references 2005–2050 — colitur bootstraps its data from lectio and uses lectio as a differential oracle), **dlectio** (offline Android app over lectio), **clectio** (tiny C build). colitur is a **standalone** tool, not part of lectio. ## The authoritative docs (read these) - **Design:** `docs/superpowers/specs/2026-07-30-colitur-design.md` — the full, approved design (scope, kernel+rites+overlays architecture, data model, output, the 5 validation layers, phasing, success criteria). **This is the contract.** - **Rules register:** `docs/research/rules-register.md` — every temporal/precedence rule the engine computes against, each citing its normative paragraph. The **EF rubrics are primary-source-verified** against the 1962 Missal (RG 91 Table of Precedence full 28 entries, occurrence RG 92–95, commemorations RG 108–111, vigils/octaves/Rogations/Sunday-classes, seasons RG 71–77). OF side cites UNLYC. - **Plans:** `docs/superpowers/plans/` — `…-plan1-computus-skeleton.md` (done). - **Primary scans:** `docs/research/*.pdf` — the 1962 Missale Romanum (Latin) and the *Rubricarum instructum* motu proprio. `docs/` is **gitignored** (research + copyrighted scans stay off the public repo). ## Binding decisions (do not relitigate) 1. **Source of truth = the 1962 Missale Romanum + its Rubricae Generales.** Divinum Officium, missalemeum, gcatholic are **comparison oracles only** — any **divergence from a reference is flagged LOUDLY** in validation, never silently swallowed. 2. **Scope is strictly the 1962 Missal** (1960 rubrics + 1955 Holy Week). Every addition (2020 *Quo Magis*/*Cum Sanctissima*, any community's proper) is an **overlay**, never core. 3. **Data is bootstrapped from lectio** (0-error vs missalemeum), then validated by **rigorous property + differential testing over a large RANDOM sample across the whole 1583–9999 range** — not only 2005–2050. 4. **EF and OF are peer rite modules** — a form is never an overlay of another form. Each has its own `temporal` + `precedence` **code** and its own **data**; they share only the kernel. 5. **Build EF end-to-end first** as the pilot vertical slice (it is the harder, more idiosyncratic form, and its rules are already fully researched), learn from it, *then* add OF as the second module to prove the `RITE` abstraction generalizes. ## Architecture (one screen) Rite-agnostic **kernel** + **rite modules** (plug in via a signature) + **data overlays**. - **Kernel** (`lib/kernel`, pure, total, bounded 1583–9999): `Computus` (Gregorian + Julian Easter + anchors), `Date` (proleptic Gregorian arithmetic), `Overlay` (ordered layer-merge algebra: field-level add/suppress/replace/edit, last-writer- wins, `empty` = identity), `Precedence` (general resolver parameterized by a rite's ruleset → observed day + commemorations + transfers; deterministic, terminating), `Calendar` (orchestrator), `Validate` (the invariant/property harness). - **Rite module** (`lib/rites/`) satisfies: ```ocaml module type RITE = sig val id : string val temporal : Date.t -> Temporal.t (* season, week, cycle, movable feasts — CODE *) val precedence : Precedence.rules (* rite ranking + resolution — CODE *) val sanctoral : Calendar.layer (* fixed-date base calendar — DATA *) val lectionary : Lectionary.t (* slug/day → citations — DATA *) end ``` Temporal + precedence are **code** (auditable, property-tested); sanctoral + lectionary are **data** (`.sexp`, bootstrapped from lectio). `Rite_ef` then `Rite_of`. - **Result type** `LiturgicalDay` = { date; rite; season; week/cycle; observed (slug, names, rank, colour, flags); commemorations; transfer; citations } — the single stable schema for all output. - **Output**: one schema → CSV / JSON / S-expression, rendered by a **logic-less Mustache-family template engine** (user supplies the target-language template; the engine never executes code). Unix-composable CLIs: `compute | render`, `table`. - **Data format**: **S-expressions** (`sexplib`/`ppx_sexp_conv`) — the OCaml type *is* the format, parse/print auto-derived, no hand-written parser. ## Validation (the "sure bet" pillar — 5 layers) 1. **Types** — illegal states unrepresentable (closed variants for ranks/colours/ seasons; dates validated at construction; resolution total). 2. **Property (QCheck), year-independent** — hold for every year 1583–9999: exactly one observed day per date; year covered once, no gaps; seasons contiguous; Easter a Sunday in [Mar 22, Apr 25]; movable feasts at correct Easter-offset weekday; overlay merge deterministic + `empty` identity; sexp round-trips. **This is how confidence extends past the oracle horizon (~2050).** 3. **Differential vs lectio** — every day 2005–2050 agrees (season, rank, observed, citations). 4. **Oracle cross-check** — vs missalemeum (EF) / litcal (OF) in lectio's `sources/`. 5. **Golden regression** — landmark + known-tricky years pinned. **Status: all five layers are built and green.** Layer 2 is exhaustively clean over all 8 417 years (`COLITUR_EXHAUSTIVE_SWEEP=1 dune test --force`, ~50 s; the default suite samples). Layer 3 compares 16 801 days against lectio; layer 4, 730 days against missalemeum; layer 5 pins ~30 dates. **Know what each layer cannot see** — this is load-bearing, not a caveat: - Layer 3 **never compares commemorations** (lectio has no RG 111 admission logic, so its "others" are losing candidates, not the admitted set) and never compares the week column. It also **shares colitur's own lineage**: colitur's data was bootstrapped from lectio, so an error both inherited is invisible here. Proven: Holy Thursday was violet in both because both were wrong. - **Layer 4's commemoration-identity gap is CLOSED** (2026-08-12, Task B, branch `ef-rg16a`, two fix rounds): it used to compare presence/count only, never *which* commemoration won — reversing `admit`'s dignity sort (the engine admitting the *worst* commemoration, an outright RG 111 violation) left all eight differential and oracle assertions green across 17 531 days. Layer 4 now also compares **identity** — colitur's own resolved English name (`Celebration.t.names`, `en`) against missalemeum's title text, for every day both streams admit the same count. The mapping resolves **every SANCTORAL-origin commemoration** (colitur's own name field, bootstrapped from lectio, verified to match missalemeum's titles character-for-character); it **cannot resolve a TEMPORAL-origin one** (an impeded feria/Ember/Rogation day — `Rite_ef.Temporal_ef` never sets a celebration name) — that case is **never silently skipped**: it is a separate, counted, allow-listed outcome (`Comm_identity_unresolved`, 19 of 227 non-empty-commemoration days over 2026–2027), not a silent pass. ONE genuine identity mismatch remains adjudicated open (register §4/§6.1, `data/ef/expected-divergences-missalemeum.sexp` M16): a known unimplemented office (the Seven Sorrows of Passion Friday). Proof of teeth, reproduced twice: reversing `admit`'s *dignity*-based sort (the historical defect shape) turns layer 4 red — an unexplained `commemoration-identity-mismatch` day (colitur admitting "St. Thecla" where both the rubric and missalemeum require "St. Linus"); separately, disabling `band`'s own `Commemoration_only` guard (below) also turns it red, on the same date this whole gap was originally found through — both reverted after confirming. - **The `admit` same-rank tie-break is RG 113, not an uncited convention** (same task, fix round 1): RG 113's own second sentence ("in admittendis et ordinandis aliis commemorationibus, servetur ordo tabellae praecedentiae"), previously quoted only in its first half, is the real rule — `admit` now orders/selects by `Precedence_ef.band` (RG 91's own table), not RG 8's coarse four-class rank. **`band` itself had a fidelity bug this exercise surfaced**: RG 91's table enumerates only "dies liturgici" (real feasts), so a `Celebration.t.status = Commemoration_only` candidate has NO row in it at all — `band` used to read `rank` alone and silently lent such a candidate the same table entry as a genuine `Feast` of its own rank, manufacturing ties RG 113 never created (the original "Maurice vs Thomas of Villanova, both entry 24" example was this bug, not a real RG 91 tie). Fixed at the source: `band` now returns `unclassified` for any `Commemoration_only` candidate, checked first. Measured, independently, twice (`compare_precedence`'s own ordering-criterion change, then `band`'s fidelity fix): the ORDERING-CRITERION change alone is zero-blast-radius (byte-identical across the whole 1583–9999 domain — a correctness-of-citation fix, not an answer-changing one); the `band`-FIDELITY fix has a real, large, fully-classified effect, **4 451 days across the whole domain, exactly 4 verified shapes, no surprises** (register §6.1). A genuine "two different candidates on the identical REAL table entry" residual was searched for exhaustively across the whole domain and found EMPTY — the tie-break `admit` still breaks alphabetically is real but narrower than first thought: it is only ever exercised between two `Commemoration_only` candidates, neither of which has any RG 91 table position to compare in the first place. ## Current state (Plans 1–3 DONE — verify with `git log`) **Plans 1 + 2 are on `main` (35 commits). Plan 3 is branch `ef-plan3`, 42 commits, 259 tests green** (260 with the exhaustive sweep). The kernel, the **complete EF temporal cycle**, the **resolution engine**, the **sanctoral data**, and **all five validation layers** are built. `colitur day ` emits a full resolved year. **Kernel** (`lib/kernel`, pure, total, 1583–9999): - `date.ml[i]` — opaque rata-die (Hinnant civil↔days); validated `make`; `to_iso8601`/`of_iso8601`; sexp form is an ISO-8601 atom that revalidates. - `computus.ml[i]` — `gregorian_easter`, `julian_easter`, Easter anchors. - Shared vocabulary: `colour` · `subject` (Lord/BVM/saint/temporal; named `Subject` because `class` is an OCaml keyword) · `slug` · `lang` · `names` (open lang-keyed assoc, canonically sorted) · `citation` · `date_spec`. - Rite-parametric: `vocab` (operations record) · `celebration` · `temporal` (+ the `RITE` module type). **`Celebration.t` takes one parameter (`'r`)**, not two — season is contextual to the day, not intrinsic to a celebration. - `layer` (slug-canonical, date-indexed once) · `overlay` (add/suppress/replace/ field-edit, ordered, last-writer-wins, **diagnostics not silence or failure**) · `record` (flat all-string output view) · `validate` (the invariant harness). - **Plan 3 additions**: `precedence` (the rite-parameterised resolver — a rite supplies `band` / `disposition` / `admit`) · `liturgical_day` (the result schema) · `rite` (everything a rite supplies, bundled, so mismatched assembly is unrepresentable) · `calendar` (**year is the primitive**, day derived — transfers need whole-year knowledge, so per-date resolution cannot be correct). **EF rite module** (`lib/rites/rite_ef`): `vocab_ef` (8 RG-cited seasons, 4 classes) · `temporal_ef` (season boundaries RG 71–77, named feasts, Sunday slugs, week numbering, the resumed-Sunday tail, ferias, four Ember sets, Rogations, `anchors`) · `precedence_ef` (the full RG 91 28-entry table, occurrence RG 92–95, commemorations RG 108–111, transfers RG 96–98) · `rite_ef` (the bundle). **Data**: `data/ef/sanctoral.sexp` (322 entries, bootstrapped from lectio, SHA-256 in its provenance header) · `data/ef/adjustments.sexp` (overlay) · two cited allow-lists, `expected-divergences.sexp` (11 entries, vs lectio) and `expected-divergences-missalemeum.sexp` (14, vs the oracle). Fixtures live in `test/fixtures/` with asserted SHA-256s. **CLI**: `colitur easter `, `temporal `, `day `. `band` is **provably total** over everything the engine constructs: zero `unclassified` across all 8 417 years, for a 28-branch hand-transcribed table. Transfers reach a fixed point everywhere — 6 739 out, 6 739 in, zero unconverged. Deps are `dune alcotest qcheck qcheck-alcotest sexplib ppx_sexp_conv` and are **frozen**. A Mustache lib is still **not** added — it arrives with rendering. **Gotcha that costs an hour if unknown:** `[@@deriving sexp]` on a type with primitive fields fails with `Unbound value string_of_sexp` unless the `.ml` opens `Sexplib0.Sexp_conv`. Every kernel module with primitive fields does. Argument-less variants (`Colour`, `Subject`) don't need it. Do **not** hand-write converters instead — that is reserved for `Slug`/`Lang`, whose `private string` smart constructors deriving would bypass. ### Build & test ```sh eval $(opam env) # activate the project-local switch (run from this dir) dune build dune test # fast suite, ~3 s COLITUR_EXHAUSTIVE_SWEEP=1 dune test --force # + every year 1583-9999, ~50 s dune exec colitur -- day 2026 | head ``` ## What's next - **Plan 4 — OF rite module** (proves `RITE` generalizes) → the lectionary bootstrap and citations → full output/rendering → hardening and a first tag. **Both behaviour items below are now RESOLVED** (RG 16(a) and commemoration identity, closed on branch `ef-rg16a`) — kept here as the record of what the five layers, taken together, used to sleep through, and as the shape a future gap of the same kind would need to be caught by. ### Carried into Plan 4 (read before starting) The full record — every task's outcome, every ruling, the 21-item deferred-minor triage, and the whole-branch review — is in `.superpowers/sdd/2026-08-11-colitur-plan3-resolution-engine/progress.md`. That workspace is deliberately kept, because it and the register corrections exist nowhere in git (`docs/` is gitignored). **The two behaviour items, in order:** 1. **RG 16(a) — RESOLVED (RG16(a) task, branch `ef-rg16a`, 2026-08-12; ONE FIX ROUND of review after the first pass — see register §6.0 for the full, corrected account).** Was the largest known-wrong output on the branch: a Feast of the Lord occurring on a II-class Sunday takes the Sunday's place *"cum omnibus iuribus et privilegiis: de dominica, proinde, **nulla fit commemoratio**"*, and colitur used to commemorate the Sunday anyway (**5 996 wrong days over 1583–9999**, 369 of them in 1583–2100, re-confirmed exactly, twice, independently). Fixed in `Rite_ef.Precedence_ef.disposition` with **no signature change** — `disposition` already took `winner:...` (RG 33's own vigil-omission branch already read it). A SECOND, related bug needed a genuine kernel signature change: `Precedence.rules.admit` gained a `~temporal` parameter, because RG 16(a) also breaks the assumption that `observed` IS the day's own temporal-cycle office for RG 111(b)'s Sunday rank-floor check (an unrelated saint could otherwise be wrongly admitted into the freed slot — confirmed on 1 178 real days, 6 August, before this second fix). **The sanctoral data question was more contested than the first pass found**: the Purification (2 Feb) was FIRST retagged `Bvm` (calendarium title argument), then REVERTED to `Lord` in fix round 1 on the user's own ruling — follow the oracle, which treats the Purification as taking an occurring Sunday's place outright, unlike an ordinary Marian feast (real primary-text counter-evidence, RG 120(b)'s colour rule, remains on record as the argument the other way). Only `most-holy-name-of-mary` stays retagged `Bvm`. A related, unresolved primary-source finding: the Common of the Dedication of a Church's own classification (*"Festum Dedicationis Ecclesiae est festum Domini"*) means St Michael's Dedication (29 Sep) may also be `Lord`, not `Saint` — measured, not applied (1 200 days domain-wide if it were). Two further open items recorded, not fixed: 13 January (Baptism of the Lord, mistagged `Saint`, and separately, RG 112 — a feast/mystery of one Divine Person excludes a commemoration of another of the SAME Person — entirely unimplemented, confirmed live on 7 real years). 2. **Commemoration identity — RESOLVED (Task B, branch `ef-rg16a`, 2026-08-12; ONE FIX ROUND of review after the first pass — see register §6.1 for the full, corrected account).** Was unasserted outside ~3 test rows — **the exact gap the RG 16(a) fix round above had exploited**: the lectio differential (layer 3) compares season/slug/rank/colour only, never commemorations, BY DESIGN (lectio has no RG 111 admission logic of its own) and still does not — that part of this item is unchanged and remains the reason layer 4, not layer 3, had to close this gap. Layer 4 (missalemeum, 2026–2027) now compares commemoration IDENTITY, not only presence/count (see the "know what each layer cannot see" section above for the mapping and its limits). While building it, found and fixed a SECOND, independent bug the exercise surfaced: `Precedence_ef.band` gave a `Commemoration_only` candidate the same RG 91 table entry as a genuine `Feast` of its own rank (RG 91's table has no row for a bare commemoration at all) — **4 451 days wrong across the whole 1583–9999 domain**, exactly 4 verified shapes, fixed at the source. The `admit` same-rank tie-break itself is RG 113 (previously uncited), not the alphabetical convention this item used to describe — reconciled against the Plan-3-era "66 days" figure: 599 is the tie POPULATION, 65 (or 67) the real ADMITTED-SET decisions within it, 149 order-only — all now independently reproduced (register §6.1), not merely asserted. **Structural items, cheapest to fix before OF exists:** 3. **`Record` and `Liturgical_day` both claim to be "the single stable output schema".** `Record` cannot express what the engine now computes (no observed celebration, no commemorations, no transfers), has **no test file**, and is used only by the legacy `colitur temporal` path — `day` hand-formats instead. Plan 2's carried item (add a `cycle` field for OF's Sunday A/B/C and weekday I/II) is still open and now costlier: it must pass through `Temporal.t`, which is embedded in the sexp-derived `Liturgical_day.t`. 4. **`Temporal.RITE` and `Rite.t` are two competing abstractions.** The module type still exists and `temporal_ef` still satisfies it, but it carries none of `rules`, `anchors`, `season_runs`, `transfer_target` — satisfying it now proves almost nothing. `Rite.t` is the load-bearing one. 5. **EF-shaped things still in "rite-agnostic" kernel code**: `validate.ml` hardcodes Sunday as the week start; `Liturgical_day.transferred_in` is an `option` justified by RG 96; `Precedence.privilege` is defined by RG 111; `Repose` is EF vocabulary emitted by nothing. Each is one field short of the remedy already applied to `season_runs`. **Data defects traced upstream into lectio's generator** (register §6): 15 entries wrongly marked `Commemoration_only` that are really III-class feasts, clustered 6 March – 5 April, **six of which produce a wrong observed office** in real years (2008-04-02/04/05, 2038-03-06/08/09); four missing entries (Agnes *secundo*, Boniface 14 May, Evaristus, Theodore); and `romanus`, which should not exist on 9 August. lectio's ini is **generated from missalemeum**, so the two are one lineage, not two independent sources. **Unbuilt, recorded**: the Sacred Triduum has no proper office (it uses ordinary Passiontide ferial slugs); RG 91 entry 27's BVM Saturday Office; RG 110's inseparable commemorations; Major Litanies (25 April); the Rogation-Wednesday commemoration. Allow-list entries M11 and M13 are `verdict open` by design. ## How to work here - **Superpowers workflow**: `brainstorming` → `writing-plans` → `executing-plans` or `subagent-driven-development`. Present a design and get approval before coding. Plans live in `docs/superpowers/plans/`, specs in `docs/superpowers/specs/`. - **TDD**, bite-sized tasks, a commit per task on the feature branch (never straight to `main` without consent). - **Every temporal/precedence rule carries a source citation** (RG/UNLYC paragraph) in a comment — grep-able, matching the register. - **Kernel is total & deterministic**: no wall-clock, randomness, or environment reads; fallible construction returns `result`/`option`, never raises on in-range input; years outside 1583–9999 rejected at the boundary. - **Commits**: conventional-commit style, subject + body only. **No AI/tool trailer of any kind** (the author considers them noise in a public repo). - **License header**: files may carry a short SPDX `AGPL-3.0-or-later` line. - `docs/` is gitignored — design/research/scans stay local; only code + README + LICENSE + this file are tracked.