1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
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 `<slug>-<cycle>`, 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:** `<Observed.Slug>-<SundayCycle>` (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
; <computed-temporal-slug>-<SundayCycle>. 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 <slug>-<cycle> 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 `<slug>-<cycle>`, 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.
|