From 5d765444f315ee512c5ceb9bcf83730be8112b60 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Mon, 27 Jul 2026 23:24:13 +0200 Subject: docs: implementation plan for OF readings (Sundays + solemnities) 3 TDD tasks: OF resolver + embedded table (seed), niedziela generator (2025-2027, All:true harvest), render verification + coverage sweep. Keyed by -. Verified caldata.Universal(), readings.Options.All (gospel-only default), and the OF slugs against current code. --- .../plans/2026-07-27-lectio-of-readings.md | 205 +++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-27-lectio-of-readings.md (limited to 'docs/superpowers') diff --git a/docs/superpowers/plans/2026-07-27-lectio-of-readings.md b/docs/superpowers/plans/2026-07-27-lectio-of-readings.md new file mode 100644 index 0000000..0e53bb3 --- /dev/null +++ b/docs/superpowers/plans/2026-07-27-lectio-of-readings.md @@ -0,0 +1,205 @@ +# 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. -- cgit v1.3