# lectio EF (1962) Liturgical Calendar Engine — Design **Status:** design, awaiting review **Date:** 2026-07-27 **Part of the self-contained-calendar epic. Follows #1 (OF engine) and #2 (custom layers); prioritized ahead of the OF-readings and API sub-projects because the tool must be universal (both forms).** ## Goal Make lectio compute the **1962 (Extraordinary Form / traditional) liturgical day** offline, so the traditional calendar no longer depends on the missalemeum scrape. Done **form-aware inside the existing engine**: `Compute(date, sel, layers)` branches on `sel.Form` — `"new"` → OF (shipped), `"old"` → EF (new). This is a universal tool; it must support both forms. Scope of this sub-project = EF **day-identity** (season, celebration, rank, colour, observed + commemorations). EF **propers/readings** (the 1-year lectionary) are a later phase, mirroring how OF split #1 (calendar) from #3-readings; the trad scraper keeps serving EF readings until then. ## Longevity mandate (global constraints) - `internal/calendar` stays **stdlib-only**; `Compute` is pure and total. - No network in the core; offline computation. - INI data + TSV corpora only; JSON/iCal for API output (later). - Data is owned plain text; reuse existing rules/data (Divinum Officium), don't reinvent. ## Architecture — one form-aware engine (option A, chosen) `Compute` selects the temporal + precedence path by `sel.Form`: ``` Compute(date, sel, layers): merged := mergeLayers(layers) # SHARED if sel.Form == "old": td := temporalEF(date) # NEW: 1962 temporal cycle ... EF candidates + precedenceEF() # NEW: 1960 rubrics else: td := temporal(date, sel) # existing OF ... precedence() # existing OF -> LiturgicalDay # SHARED result type ``` **Shared (unchanged):** Computus (`Easter`), `mergeLayers`, `DateSpec`/`resolveDate`, the INI data format, `LiturgicalDay`, and the whole custom-layer stack (#2) — so a diocese/order layer works for EF exactly as for OF. **New, EF-specific:** `temporalEF`, `precedenceEF`, the EF rank vocabulary, and the embedded EF sanctoral data. ## Type model ### Rank: int enum → unified string token (prerequisite refactor) `Rank` becomes `type Rank string`. Both vocabularies coexist; each form's precedence interprets its own: - **OF:** `solemnity`, `feast`, `memorial`, `optional`, `ferial` (unchanged values, now strings). - **EF (1960 Code of Rubrics):** `class-1`, `class-2`, `class-3`, `class-4`, `commemoration`, `ferial`. (I–IV class + commemoration — the 1962 scheme; matches the missalemeum oracle's numeric `rank: 1–4`. NOT the pre-1960 Double/Semidouble/Simple system.) Ordered comparisons that today use `<` (e.g. `cel.Rank < RankSolemnity` in OF Compute filters) become an explicit **per-form ordering helper** `ofRankOrder(Rank) int` / `efRankOrder(Rank) int`. `ParseRank` becomes a passthrough/validator; `rankLabel` (CLI display) becomes form-aware. This refactors shipped OF code at ~5 comparison sites + `precedence.go` + `temporal.go` constructors + `caldata` parsing + tests. It is **guarded by re-running the OF oracle** (0 season mismatches must hold). ### Season: superset string (no refactor) `Season` is already `type Season string`. Add EF constants, sharing advent/christmas/lent/easter/triduum: `septuagesima`, `passiontide`, `time-after-epiphany`, `time-after-pentecost`, plus octave handling. OF code emits OF seasons; EF code emits EF (+shared) seasons. ### Colour / Class `Colour` (white/red/green/violet/rose/black) already covers EF. `Class` stays OF-only; EF precedence reads its own nature tags (feria/vigil/octave/feast-of-lord…) from the data + temporal. ## The EF (1962) temporal cycle (`temporalEF`) Derived from Computus + the Advent anchor (reuses `adventStart`). Seasons and their movable structure: - **Advent** — 4 Sundays (shared start). - **Christmastide** — Christmas + octave (Dec 25–Jan 1), Circumcision (Jan 1), Most Holy Name (Sun Jan 2–5, else Jan 2), Epiphany (Jan 6) + octave (to Jan 13), Holy Family (Sun in the octave). - **Time after Epiphany** — up to 6 Sundays; the surplus are *resumed* before the last Sunday after Pentecost. - **Pre-Lent** — Septuagesima (`easter-63`), Sexagesima, Quinquagesima (violet, no Alleluia). - **Lent** — Ash Wednesday (`easter-46`) → Sundays I–IV. - **Passiontide** — Passion Sunday (`easter-14`), Palm Sunday (`easter-7`). - **Holy Week / Triduum**. - **Paschaltide** — Easter + octave, Low Sunday (`easter+7`), Sundays II–V after Easter, Rogation days, Ascension (`easter+39`) + octave, Sunday after the Ascension, Pentecost (`easter+49`) + octave (Ember days). - **Time after Pentecost** — Trinity (`easter+56`), Corpus Christi (Thu, `easter+60`), Sacred Heart (Fri, `easter+68`); Sundays 2nd–24th; **resumed Time-after-Epiphany Sundays** inserted before the Last Sunday when the year has >24 such weeks. **Christ the King = last Sunday of October** (EF rule — *different* from OF's last-Sunday-before-Advent). Ferias, vigils, and octave days are temporal celebrations with their own EF rank (I–IV class feria, etc.). ## The 1960 precedence (`precedenceEF`) Implements the *Rubricarum instructum* (1960 Code of Rubrics) **occurrence** rules: when two celebrations fall on one day, the higher class is observed; the other is commemorated, omitted, or transferred per the table (I-class feasts transfer when impeded; a III-class feast on a privileged feria is commemorated; etc.). Concurrence (Vespers ranking) is out of scope — we compute the **Mass day identity**. The engine returns the observed celebration + the commemorations (`LiturgicalDay.Others`). ## Data — embedded 1962 sanctoral `internal/caldata` gains `//go:embed tridentine-calendar.ini` and `Tridentine() calendar.Layer` (the EF base). Bootstrapped from the 1962 General Roman Calendar via **Divinum Officium** data (through the **Missal1962** / **tridentine_calendar** ports); credited in NOTICE. Entries carry EF ranks (`class-1…4`), fixed dates, colours, names — the EF sanctoral differs from OF in dates, ranks, and membership. Initial subset ships; full population is oracle-gated (like #1). `caldata.Stack` becomes form-aware: `Stack(form, dir, use)` returns `[Base(form)] + user layers`, where `Base("old") = Tridentine()`, else `Universal()`. Custom user layers (#2) stack over whichever base the form selects. ## CLI The `--liturgy` EF guard (added earlier) is **removed**: `sel.Form == "old"` now computes the EF day. `rankLabel` shows EF ranks. Everything else (`--cal-new`/`--cal-check`, the `use` stack) already works for both forms. ## Validation — the missalemeum oracle `scripts/build-oracle-ef.sh` snapshots missalemeum's per-date API (`https://www.missalemeum.com/en/api/v5/proper/{date}`) over a range (e.g. 2024–2028) into `internal/calendar/testdata/oracle-ef.json` as `{date: {season, rank, colour}}`, mapping its fields: - `tempora` string (e.g. "Feria VI after IX Sunday after Pentecost") → our EF season + week. - `rank` (1–4) → `class-1…4`; `colors` (`w/r/v/g/…`) → our colours. - top `info`/celebration → the observed. `oracle_ef_test.go` computes each day and asserts **season strictly** (data-independent, validates `temporalEF`), reporting rank/colour diffs informationally (partial sanctoral). Same offline-snapshot pattern as #1. **Not run in `go test` without the snapshot** (network-gated generator). Also: **re-run the OF oracle** after the Rank refactor — it must still show 0 season mismatches. ## Error handling `Compute` stays total for both forms. Malformed embedded EF data → fail fast at init (test-caught). Unknown `sel.Form` → default to OF (documented). ## Package/file layout | Concern | File | Change | |---|---|---| | Rank refactor | `internal/calendar/types.go`, `precedence.go`, `temporal.go`, `calendar.go` | int enum → string + ordering helpers | | EF temporal | `internal/calendar/temporal_ef.go` **(new)** | `temporalEF` + EF seasons | | EF precedence | `internal/calendar/precedence_ef.go` **(new)** | 1960 rubrics | | Form branch | `internal/calendar/calendar.go` | `Compute` selects OF/EF path | | EF data | `internal/caldata/tridentine-calendar.ini` **(new)**, `caldata.go` | `Tridentine()`, form-aware `Stack` | | CLI | `internal/cli/liturgy.go` | drop the EF guard; form-aware rank labels | | Oracle | `internal/calendar/oracle_ef_test.go`, `testdata/oracle-ef.json`, `scripts/build-oracle-ef.sh` **(new)** | missalemeum validation | ## Non-goals (this sub-project) EF propers/readings (the 1-year lectionary — a later phase; scraper serves them meanwhile); full EF sanctoral population; the OF readings (#3-OF); the API (#4); migrating the daily-readings view off the scraper. ## Risks / open questions - **Time after Pentecost + resumed Epiphany Sundays** and **Christ the King (last Sun of October)** are the fiddliest temporal bits; the oracle is the safety net (confidence ~6/10 on the first cut, ~8/10 on the design). - The **Rank string refactor** touches validated OF code — mitigated by the OF oracle re-run as an acceptance gate. - `tempora`-string → season/week parsing for the oracle needs a small mapping table (English ordinals, "after Pentecost/Epiphany", pre-Lent names).