summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/superpowers/specs/2026-07-27-lectio-ef-calendar-engine-design.md128
1 files changed, 128 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-07-27-lectio-ef-calendar-engine-design.md b/docs/superpowers/specs/2026-07-27-lectio-ef-calendar-engine-design.md
new file mode 100644
index 0000000..8736a90
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-27-lectio-ef-calendar-engine-design.md
@@ -0,0 +1,128 @@
+# 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).