# OF Readings (Sundays + Solemnities) Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Serve OF Sunday & solemnity readings offline — an embedded `of-lectionary.ini` keyed by `-`, generated from niedziela over 2025-2027, resolved via an OF branch in `caldata.Readings`, rendered from the public-domain corpora (Polish → Latin). **Architecture:** Mirror the EF readings pipeline. `caldata` embeds `of-lectionary.ini` (parsed once into `ofTempReadings`); `TemporalReadings` dispatches EF→`efTempReadings` / OF→`ofTempReadings`; `Readings` looks the OF table up by `Observed.Slug + "-" + SundayCycle`. A `//go:build ignore` generator produces the table from niedziela. Rendering is unchanged (`readingLine`). **Spec:** `docs/superpowers/specs/2026-07-27-lectio-of-readings-design.md` ## Global Constraints - **Scope: Sundays + solemnities only.** Weekdays (2-year cycle) and sanctoral memorials are OUT. - **Keying:** `-` (e.g. `ordinary-sunday-10-A`, `christ-the-king-B`, `assumption-A`). Uniform for Sundays and all solemnities (`SundayCycle` is populated on every day — verified). - **Citations only**, English-canonical (normalized via `bible.ToEnglishRef`); text rendered from `drb`/`vul`; Polish → Latin fallback (unchanged `readingLine`). - **EF path unchanged**; the niedziela daily-view scraper unchanged; OF readings simply start resolving where they were empty. - **`internal/calendar` untouched.** Readings live in `caldata` (imports `calendar` + `ini`; the `//go:build ignore` generator may import more). - Every non-generator task ends `gofmt -l` clean and `go test ./...` green. **Verified facts (use as written):** - `caldata.parseLectionary(data []byte) map[string][]calendar.Reading` reads INI `[slug]` sections, fields `first`/`psalm`/`second`/`gospel` → `[]calendar.Reading{Part,Citation}`. - `var efTempReadings = parseLectionary(tridentineLectionary)`; `func TemporalReadings(form, slug string) []calendar.Reading { if form=="old" { return efTempReadings[slug] } … }`. - `caldata.Readings(sel, layers, date, day)` today: inline `day.Observed.Masses` readings → `TemporalReadings(sel.Form, day.Observed.Slug)` → EF-only weekday fallback → nil. - OF `LiturgicalDay` carries `SundayCycle` ("A"/"B"/"C") on every day; slugs are `ordinary-sunday-10`, `lent-sunday-3`, `easter-sunday`, `christ-the-king`, `baptism-of-the-lord`, `trinity-sunday`, `palm-sunday`, plus sanctoral solemnity names. - `readings.Load(cfg config.Config, opts readings.Options) ([]liturgy.Section, liturgy.DayInfo, error)`; `liturgy.Section{Heading, Subtitle, Citation, PartID string}`; PartIDs `pierwsze_czytanie`/`psalm`/`drugie_czytanie`/`ewangelia`. - `bible.ToEnglishRef(plRef, system string) (string, error)` converts a Polish citation to English-canonical; `scripts/genlect.go` has `cleanCite` for source glitches. --- ### Task 1: OF resolver + embedded table (with a seed) **Files:** - Create: `internal/caldata/of-lectionary.ini` (small hand-authored seed) - Modify: `internal/caldata/caldata.go` (embed + `ofTempReadings` + `TemporalReadings` dispatch), `internal/caldata/readings.go` (OF key) - Test: `internal/caldata/readings_test.go` (add OF cases), `internal/caldata/of_test.go` **Interfaces:** - Produces: OF-aware `TemporalReadings(form, slug)`; `Readings` resolving OF by slug+cycle. - [ ] **Step 1: Seed** `internal/caldata/of-lectionary.ini` with a few STABLE, well-known cycle-A Sundays/solemnities (the generator replaces this file in Task 2; these keys will still be present after, so Task-1 tests keep passing): ```ini ; OF (Ordinary Form) Sunday & solemnity lectionary, keyed by ; -. Citations English-canonical. ; SEED — replaced by scripts/genlect-of.go (niedziela, 2025-2027). [easter-sunday-A] first = Acts 10:34,37-43 psalm = Ps 118:1-2,16-17,22-23 second = Col 3:1-4 gospel = John 20:1-9 [christ-the-king-A] first = Ezek 34:11-12,15-17 psalm = Ps 23:1-2,2-3,5-6 second = 1 Cor 15:20-26,28 gospel = Matt 25:31-46 ``` - [ ] **Step 2: Write the failing test** `internal/caldata/of_test.go`: ```go package caldata import ( "testing" "time" "github.com/lukaszkasprzak/lectio/internal/calendar" ) func TestOFReadingsByCycle(t *testing.T) { // A synthetic OF day keyed christ-the-king-A must resolve to Matt 25. day := calendar.LiturgicalDay{ Observed: calendar.Celebration{Slug: "christ-the-king", Layer: "temporal"}, SundayCycle: "A", Weekday: time.Sunday, } sel := calendar.Selection{Form: "new"} rs := Readings(sel, nil, time.Date(2026, 11, 22, 0, 0, 0, 0, time.UTC), day) var gospel string for _, r := range rs { if r.Part == "gospel" { gospel = r.Citation } } if gospel == "" || gospel[:4] != "Matt" { t.Fatalf("christ-the-king-A gospel = %q, want Matt…", gospel) } } func TestOFWeekdayEmpty(t *testing.T) { day := calendar.LiturgicalDay{ Observed: calendar.Celebration{Slug: "ordinary-weekday", Layer: "temporal"}, Weekday: time.Wednesday, // no SundayCycle key -> deferred phase -> empty } if rs := Readings(calendar.Selection{Form: "new"}, nil, time.Now().UTC(), day); len(rs) != 0 { t.Fatalf("weekday should be empty, got %v", rs) } } ``` - [ ] **Step 3: Run it, verify it fails** — `go test ./internal/caldata/ -run TestOF` → FAIL. - [ ] **Step 4: Embed + parse** in `internal/caldata/caldata.go` (next to the tridentine block): ```go //go:embed of-lectionary.ini var ofLectionary []byte // ofTempReadings maps an OF - key to its readings (parsed once). var ofTempReadings = parseLectionary(ofLectionary) ``` - [ ] **Step 5: Dispatch in `TemporalReadings`** — for the OF, use `ofTempReadings`: ```go func TemporalReadings(form, slug string) []calendar.Reading { if form == "old" { return efTempReadings[slug] } return ofTempReadings[slug] } ``` - [ ] **Step 6: OF key in `Readings`** — replace the single temporal lookup with a form-aware key: ```go // temporal table: EF by slug, OF by slug+cycle. key := day.Observed.Slug if sel.Form != "old" && day.SundayCycle != "" { key = day.Observed.Slug + "-" + day.SundayCycle } if r := TemporalReadings(sel.Form, key); len(r) > 0 { return r } ``` (Leave the EF weekday fallback block below it untouched.) - [ ] **Step 7: Run tests** — `go test ./internal/caldata/` PASS; also `go test ./...` (EF unchanged) PASS. - [ ] **Step 8: gofmt + commit** — `git commit -m "feat(caldata): OF Sunday/solemnity readings by slug+cycle (seed table)"` --- ### Task 2: Generator + full lectionary table **Files:** - Create: `scripts/genlect-of.go` (`//go:build ignore`) - Regenerate: `internal/caldata/of-lectionary.ini` **Interfaces:** consumes `calendar.Compute`, `readings.Load`, `bible.ToEnglishRef`. - [ ] **Step 1: Write** `scripts/genlect-of.go` (`//go:build ignore`; `go run scripts/genlect-of.go`), modeled on `scripts/genlect.go`: - `sel := calendar.DefaultSelection(); sel.Form = "new"`; layers `[]calendar.Layer{caldata.Universal()}`. - Iterate every day 2025-01-01 .. 2027-12-31. Compute the day. KEEP it if `date.Weekday()==time.Sunday` OR `day.Observed.Rank` is a solemnity/feast-of-the-Lord (`RankSolemnity`/`RankFeast` with proper readings). Skip ordinary weekdays. - Key = `day.Observed.Slug + "-" + day.SundayCycle`. Skip if already seen. - Fetch citations: build a modern-lectionary `config.Config` (lectionary "new"), `readings.Load(cfg, readings.Options{Date: date.Format("2006-01-02"), All: true})` — **`All: true` is REQUIRED** (the default keeps only the gospel); for each returned `liturgy.Section`, map `PartID` → part (`pierwsze_czytanie`→first, `psalm`→psalm, `drugie_czytanie`→second, `ewangelia`→gospel); normalize `section.Citation` via `bible.ToEnglishRef(cit, system)` (psalm→"drb", else default/"") + reuse a `cleanCite` copy from genlect.go. Skip parts whose citation is empty or fails to normalize (log them). - Store `map[key]map[part]citation`; require at least first+gospel to keep the entry. - Write sorted into `internal/caldata/of-lectionary.ini` (same header/format as the seed). Log: entries written, and every kept day whose first or gospel was missing/unparseable (visible gaps, never silent). - [ ] **Step 2: Run the generator** — `go run scripts/genlect-of.go` (network; ~200 fetches). Confirm it writes `of-lectionary.ini` with a plausible entry count (roughly (52 Sundays × 3 cycles) + solemnities ≈ 170-200 entries) and review the logged gaps. - [ ] **Step 3: Build + coverage check** — `go build ./...`; `go test ./internal/caldata/` (Task-1 tests still pass against the regenerated table — `christ-the-king-A` gospel is still Matt 25). Spot-check via a throwaway: `Readings` for a 2026 cycle-A Sunday resolves first+gospel. - [ ] **Step 4: gofmt + commit** — `gofmt -w scripts/genlect-of.go`; `git add scripts/genlect-of.go internal/caldata/of-lectionary.ini`; `git commit -m "feat(caldata): generate OF Sunday/solemnity lectionary from niedziela (2025-2027)"` --- ### Task 3: Render verification + coverage sweep **Files:** - Test: `internal/cli/liturgy_test.go` (add an OF render case) **Interfaces:** consumes the built resolver + table. - [ ] **Step 1: Write the test** in `internal/cli/liturgy_test.go`: with an EN modern config, `dayReadings`/`runLiturgy` for a 2026 cycle-A Sunday (e.g. `2026-06-07`, Ordinary Sunday 10) returns a non-empty first and gospel; assert the gospel citation is non-empty. (Use the same helpers the existing liturgy tests use.) - [ ] **Step 2: Run it** — `go test ./internal/cli/` PASS. - [ ] **Step 3: Manual acceptance** (build a binary, temp configs — never touch the real config): - EN: `lectio 2026-06-07 -L` shows a first reading + gospel with text (drb). - PL: a `traditional_lang=pl`/`ui_language=pl` modern config renders the Latin fallback note + Latin text. - Coverage sweep: every Sunday of 2026 (cycle A) resolves a non-empty first + gospel; print any gaps. - [ ] **Step 4: gofmt + commit** — `git commit -m "test(cli): OF Sunday readings render offline (en text, pl Latin fallback)"` --- ## Wrap-up - [ ] `go test ./... && gofmt -l internal/ scripts/ cmd/`. - [ ] Bump `config.Version` (0.35.0 → 0.36.0). - [ ] NOTICE/README: credit niedziela.pl for the OF lectionary citations; note OF Sunday/solemnity readings now render offline (weekdays pending). - [ ] Update memory `lectio-selfcontained-calendar-epic.md`: OF Sunday+solemnity readings shipped; the self-contained epic's four sub-projects are complete (weekdays/sanctoral = follow-on phases). - [ ] Use **superpowers:finishing-a-development-branch** to merge/install. - [ ] Manual: the JSON/iCal API for a 2026 Sunday now carries `readings` (they flow through `caldata.Readings` automatically). ## Self-Review Notes - Spec coverage: table+resolver (T1), generator (T2), render+sweep (T3). Keying `-`, Polish→Latin, EF-unchanged all mapped. - Regeneration safety: Task-1 tests assert stable facts (`christ-the-king-A` → Matt 25) that survive Task 2 overwriting `of-lectionary.ini`; the weekday-empty test needs no table data. - Confirm-before-coding: read the exact `Readings` body and `readings.Load`/`Options` shape (T2) from current code; confirm `caldata.Universal()` is the OF base-layer constructor (it is used by the EF `Tridentine()` sibling). - The generator is network-dependent and `//go:build ignore` (not unit-tested); its output is validated by Task 1's tests + Task 3's sweep. Log gaps, never drop silently.