summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/superpowers/plans/2026-07-23-lectio-go-rewrite.md140
-rw-r--r--docs/superpowers/specs/2026-07-23-lectio-go-rewrite-design.md106
2 files changed, 236 insertions, 10 deletions
diff --git a/docs/superpowers/plans/2026-07-23-lectio-go-rewrite.md b/docs/superpowers/plans/2026-07-23-lectio-go-rewrite.md
index 84e5c32..b4a82d2 100644
--- a/docs/superpowers/plans/2026-07-23-lectio-go-rewrite.md
+++ b/docs/superpowers/plans/2026-07-23-lectio-go-rewrite.md
@@ -1371,6 +1371,146 @@ git add -A && git commit -m "docs: README; finalize build"
---
+## Addendum A: Traditional lectionary (missalemeum)
+
+Adds `lectionary = new|traditional`. Traditional propers come from the
+missalemeum API. Integrates via the shared `[]Section` type. Do these after the
+base plan (or interleave: A1–A2 before Task 11).
+
+### Amendment to Task 7 (Section type)
+
+Add a `PartID string` field to `Section`:
+```go
+type Section struct {
+ Heading, Subtitle, Citation, PartID string
+ Paragraphs [][]string
+}
+```
+In modern `Parse`, set `PartID` from the heading: map `1. czytanie`→`pierwsze_czytanie`
+(or `drugie_czytanie` for a second `1. czytanie` occurrence), `Psalm`→`psalm`,
+`Aklamacja`→`aklamacja`, `Ewangelia`→`ewangelia`.
+
+### Amendment to Task 10 (config)
+
+Add fields and `PartShown`; extend the embedded seed with the commented
+`lectionary`, `traditional_lang`, and `[parts.*]` blocks from the spec.
+```go
+type Config struct {
+ SchemaVersion int `toml:"schema_version"`
+ Lectionary string `toml:"lectionary"`
+ TraditionalLang string `toml:"traditional_lang"`
+ Versions []string `toml:"versions"`
+ DefaultVersion string `toml:"default_version"`
+ Width int `toml:"width"`
+ All bool `toml:"all"`
+ Offline bool `toml:"offline"`
+ Parts map[string]map[string]bool `toml:"parts"`
+}
+
+// PartShown reports whether a part renders: true unless explicitly set false.
+func (c Config) PartShown(lectionary, partID string) bool {
+ if m, ok := c.Parts[lectionary]; ok {
+ if v, ok := m[partID]; ok {
+ return v
+ }
+ }
+ return true
+}
+```
+Defaults: `Lectionary="new"`, `TraditionalLang="pl"`. Validate `Lectionary` ∈
+{new,traditional}, `TraditionalLang` ∈ {pl,en}. Add a `TestPartShown` (empty →
+all true; `{new:{psalm:false}}` → psalm false, ewangelia true).
+
+### Amendment to Tasks 8–9 (Load router)
+
+`Load(cfg config.Config, opts Options) ([]Section, error)`: if
+`cfg.Lectionary=="traditional"`, delegate to `tradlit.Load(opts.Date,
+cfg.TraditionalLang)` (offline: `tradlit.LoadOffline`); else the modern path.
+Then filter: `sections = keep(s for s where cfg.PartShown(cfg.Lectionary, s.PartID))`,
+unless `opts.GospelOnly`, which keeps only `ewangelia`/`evangelium`. Traditional
+cache lives under `traditional/{date}.json`; harvest writes `sigla-traditional.tsv`.
+
+### Task A1: internal/tradlit — fetch + parse missalemeum propers
+
+**Files:** Create `internal/tradlit/tradlit.go`, `internal/tradlit/parse_test.go`,
+`internal/tradlit/testdata/2026-07-22.json` (save `curl -s https://www.missalemeum.com/en/api/v5/proper/2026-07-22`).
+
+**Interfaces:**
+- Consumes: `liturgy.Section`.
+- Produces: `tradlit.Parse(jsonBody []byte) ([]liturgy.Section, error)`;
+ `tradlit.Load(date, lang string) ([]liturgy.Section, error)`.
+
+- [ ] **Step 1: Save fixture + write the failing test**
+
+```bash
+mkdir -p internal/tradlit/testdata
+curl -s -A 'Mozilla/5.0' https://www.missalemeum.com/en/api/v5/proper/2026-07-22 \
+ > internal/tradlit/testdata/2026-07-22.json
+```
+
+`internal/tradlit/parse_test.go`:
+```go
+package tradlit
+
+import (
+ "os"
+ "testing"
+)
+
+func TestParse(t *testing.T) {
+ body, _ := os.ReadFile("testdata/2026-07-22.json")
+ secs, err := Parse(body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var gospel, epistle bool
+ for _, s := range secs {
+ if s.PartID == "evangelium" {
+ gospel = true
+ if s.Citation != "Luke 7:36-50" {
+ t.Errorf("gospel citation = %q", s.Citation)
+ }
+ if len(s.Paragraphs) == 0 {
+ t.Error("gospel has no vernacular text")
+ }
+ }
+ if s.PartID == "lectio" {
+ epistle = true
+ }
+ }
+ if !gospel || !epistle {
+ t.Errorf("missing parts: gospel=%v epistle=%v", gospel, epistle)
+ }
+}
+```
+
+- [ ] **Step 2: Run test, verify it fails**
+
+Run: `go test ./internal/tradlit/` — Expected: FAIL.
+
+- [ ] **Step 3: Implement tradlit.go**
+
+Unmarshal `[]struct{ Info struct{Title string} `json:"info"`; Sections []struct{ ID, Label string; Body [][]string } }`. For each section: `PartID = strings.ToLower(ID)`, `Heading = Label`, join `Body[0]` into `Paragraphs`, and extract the citation from the first `*...*` marker in the body text (`regexp.MustCompile(`\*([^*\n]+)\*`)`). Skip empty/administrative sections. `Load` fetches `https://www.missalemeum.com/{lang}/api/v5/proper/{date}` with the User-Agent and calls `Parse`; a 404 means "no propers for this date". The citation is already kjv-style (e.g. `Luke 7:36-50`, `Ps 44:2`), so `bible.Lookup` consumes it directly; psalm citations are Vulgate-numbered (1962 Missal) — feed the version lookup as-is for `vul/grb/wuj`; `drb` psalms may sit a verse off (same known limitation as the modern mode, no extra handling).
+
+- [ ] **Step 4: Run tests, verify pass**
+
+Run: `go test ./internal/tradlit/` — Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add internal/tradlit && git commit -m "tradlit: missalemeum 1962 propers -> sections"
+```
+
+### Amendment to Task 11 (render) and Task 12 (cli)
+
+`render.GatherVersion`: for a traditional section, the vernacular source column is
+`sec.Paragraphs` (used for the `pl` role / when a part has no citation); the four
+bible versions use `sec.Citation` when present. A prayer part (no citation)
+renders vernacular only, with a one-line note that Latin is unavailable. `cli`:
+add global `--lectionary`/`--lang` flags overriding config; everything else is
+unchanged because both sources yield `[]Section`.
+
## Self-Review Notes
- Spec coverage: two binaries (T1,12,13,15-via-14), embedded corpora (T1,4), lookup (T4,5), aliases incl. `J`→John (T3), citation conversion + psalm systems (T2,6), fetch/parse (T7,8), cache HTML+JSON (T8), sigla harvest + offline (T9), config incl. `offline` (T10), render + dedup + pl→wuj (T11), subcommands (T12), colored reader TUI (T13), Makefile/cross/README (T1,14). All spec sections map to a task.
diff --git a/docs/superpowers/specs/2026-07-23-lectio-go-rewrite-design.md b/docs/superpowers/specs/2026-07-23-lectio-go-rewrite-design.md
index f0889e3..2c6824b 100644
--- a/docs/superpowers/specs/2026-07-23-lectio-go-rewrite-design.md
+++ b/docs/superpowers/specs/2026-07-23-lectio-go-rewrite-design.md
@@ -51,7 +51,8 @@ Module: `github.com/lukaszkasprzak/lectio`. Location: `~/git/projects/lectio`.
```
cmd/lectio/main.go -> cli.Run(args, stdin, stdout, stderr) int
cmd/lectio-ui/main.go -> load config+data, tui.New(...), tea.NewProgram(...).Run()
-internal/liturgy/ fetch + parse niedziela.pl; cache; sigla harvest/offline
+internal/liturgy/ Section type; fetch + parse niedziela.pl; cache; harvest/offline; Load router
+internal/tradlit/ missalemeum API: fetch + parse 1962 propers into []Section
internal/bible/ embedded corpora + reference lookup + book aliases
internal/psalter/ psalm versification (port of psalm_versify.py)
internal/render/ text rendering: compare columns, section text (CLI)
@@ -103,7 +104,10 @@ subset the tool actually feeds it:
(`tabnowy0all`), fall back to `tabstary0all` with a warning; extract each
section's heading, subtitle, citation, and paragraphs. Fail loudly if the tab
is present but empty or absent (layout change).
-- `Section{Heading, Subtitle, Citation, Paragraphs}`.
+- `Section{Heading, Subtitle, Citation, PartID string; Paragraphs [][]string}`
+ (shared by both lectionaries; `PartID` drives part-filtering — see Config).
+- `Load(cfg, opts) ([]Section, error)` routes to this modern source or to
+ `tradlit` by `cfg.Lectionary`, then applies part-filtering.
### Caching (fast repeat loads)
Two layers under `~/.cache/lectio/` (honor `XDG_CACHE_HOME`):
@@ -145,6 +149,42 @@ Tysiąclecia) is online-only and not embedded (copyright); **offline, `wuj`
replaced by `wuj` where needed (see Config → `offline`). A date absent from the
harvest gives a clear "run `lectio update` online" error.
+## Lectionaries: new vs traditional
+
+The `lectionary` config selects the source; both produce a `[]Section`, so the
+render/CLI/TUI/cache layers are shared.
+
+- **`new`** (default) — the modern Ordinary-Form readings from niezbednik.niedziela.pl
+ (`internal/liturgy`), as described above.
+- **`traditional`** — the 1962-Missal propers from the missalemeum API
+ (`internal/tradlit`): `GET https://www.missalemeum.com/{lang}/api/v5/proper/{date}`,
+ `lang` = `traditional_lang` (`pl` or `en` — the only two the API serves). The
+ JSON is `[{info{title,...}, sections:[{id,label,body:[["...text..."]]}]}]`. Each
+ section becomes a `Section{Heading:label, PartID:lower(id), Paragraphs, Citation}`;
+ the citation is extracted from the `*Book c:v*` marker embedded in a reading's
+ body. Section ids: `Introitus, Oratio, Lectio, Graduale, Evangelium,
+ Offertorium, Secreta, Communio, Postcommunio` (plus Alleluia/Tractus/commemorations).
+
+**How the versions apply.** A traditional `Section` carries missalemeum's
+vernacular text as its `Paragraphs` (the `pl`-role source text) and, for scripture
+propers, a `Citation`. So the scripture parts (Introit, Epistle, Gradual, Gospel,
+Offertory, Communion) flow through the *same* version-compare as the modern mode —
+`vul` (Latin), `wuj`, `grb`, `drb` looked up from the embedded corpora by citation.
+The prayer parts (Collect, Secret, Postcommunion) have no citation, so they render
+only the missalemeum vernacular.
+
+**Latin caveat.** The missalemeum v5 API returns vernacular only (en/pl) — no
+Latin. Latin for the *readings* comes from the embedded `vul` corpus via the
+citation; Latin for the *prayers* is not available and those show vernacular only.
+This is a documented limitation, surfaced with a short note on prayer sections.
+
+**Caching / offline.** Traditional pages cache like modern ones (`{date}.json`
+under a `traditional/` subdir so the two lectionaries don't collide). `lectio
+update` harvests whichever lectionary is active into its own sigla file
+(`sigla.tsv` / `sigla-traditional.tsv`); offline builds the scripture-part
+sections from the harvested citations. Prayer text is not harvested (it is not a
+citation), so offline traditional shows the readings only.
+
## Versions and psalm systems
Ported from `ewangelia.py` constants:
@@ -202,6 +242,8 @@ lectio help | lectio -h | lectio <cmd> -h
- `--refresh` bypasses the cache and re-fetches.
- `--offline` (global) skips the network and uses the sigla TSV; also applied
automatically when a fetch fails.
+- `--lectionary new|traditional` and `--lang pl|en` (global) override the config
+ `lectionary` / `traditional_lang` for one run.
- `update` harvests forward from today (or `--from D`) up to `--days N` (default:
until the unpublished horizon), writing the sigla TSV.
- Flags override config. Exit codes: 0 ok, 1 runtime error (fetch/parse), 2
@@ -252,16 +294,55 @@ fixed. The CLI stays plain text (colour is a TUI concern).
run, honoring `XDG_CONFIG_HOME`) → built-in defaults. Flags override.
```toml
-schema_version = 1
-versions = ["pl", "wuj", "vul", "grb", "drb"] # compare set + TUI cycle order
-default_version = "pl" # TUI start / `lectio show` default
-width = 0 # CLI wrap width; 0 = detect terminal
-all = false # default to all readings (true) or just the gospel (false)
-offline = false # true = never fetch; read only harvested sigla + cache
+schema_version = 1
+lectionary = "new" # "new" (niedziela.pl) or "traditional" (missalemeum, 1962)
+traditional_lang = "pl" # vernacular for traditional propers: "pl" or "en"
+versions = ["pl", "wuj", "vul", "grb", "drb"] # compare set + TUI cycle order
+default_version = "pl" # TUI start / `lectio show` default
+width = 0 # CLI wrap width; 0 = detect terminal
+all = false # default to all parts (true) or just the gospel (false)
+offline = false # true = never fetch; read only harvested sigla + cache
+
+# Which parts to show. Both tables are commented out -> every part is shown.
+# Uncomment a table and set a part to false to hide it; parts you don't list
+# stay shown. (Parsed as a map: a part is hidden only if explicitly false.)
+#
+# [parts.new]
+# pierwsze_czytanie = true # 1. czytanie (1st reading)
+# psalm = true # Psalm
+# drugie_czytanie = true # 2. czytanie (2nd reading, on feasts)
+# aklamacja = true # Aklamacja (acclamation)
+# ewangelia = true # Ewangelia (gospel)
+#
+# [parts.traditional]
+# introitus = true # Introit
+# oratio = true # Collect
+# lectio = true # Epistle
+# graduale = true # Gradual / Alleluia / Tract
+# evangelium = true # Gospel
+# offertorium = true # Offertory
+# secreta = true # Secret
+# communio = true # Communion
+# postcommunio = true # Postcommunion
```
-Unknown versions in config are rejected with a clear error. Invalid TOML falls
-back to defaults with a stderr warning.
+`Config` fields: `SchemaVersion int`, `Lectionary string`, `TraditionalLang
+string`, `Versions []string`, `DefaultVersion string`, `Width int`, `All bool`,
+`Offline bool`, and `Parts map[string]map[string]bool` (the `[parts.new]` /
+`[parts.traditional]` tables). `Lectionary` must be `new` or `traditional`;
+`TraditionalLang` must be `pl` or `en`; unknown versions/values are rejected with
+a clear error. Invalid TOML falls back to defaults with a stderr warning.
+
+**Part filtering.** `Config.PartShown(lectionary, partID string) bool` returns
+`true` unless `Parts[lectionary][partID]` is present and `false`. So a commented
+(empty) `Parts` shows everything, and a user hides a part by uncommenting the
+table and setting just that part to `false`. Part IDs: for `new`,
+`pierwsze_czytanie`/`psalm`/`drugie_czytanie`/`aklamacja`/`ewangelia` (matched
+from the niedziela heading); for `traditional`, the missalemeum section ids
+lower-cased (`introitus`, `oratio`, `lectio`, `graduale`, `evangelium`,
+`offertorium`, `secreta`, `communio`, `postcommunio`). Filtering applies whenever
+all parts are in play (`--all`, `compare`, the TUI); the gospel-only default
+(`all = false`) always shows just the gospel / `evangelium`.
`offline = true` (or the `--offline` flag) makes lectio work purely from the
`lectio update` harvest and cache — it never touches the network, and `pl`
@@ -311,6 +392,11 @@ network-free daily read.
- **psalter**: unit tests for `drb_verse` on titled/untitled/2-line-title psalms.
- **liturgy.Parse**: fixture tests against a few cached HTML pages (a normal day,
a split-reading feast, an unpublished date), asserting sections/citations.
+- **tradlit.Parse**: fixture test against a saved missalemeum JSON response
+ (2026-07-22), asserting the proper sections, part ids, the extracted Gospel
+ citation, and vernacular paragraphs.
+- **config.PartShown**: a commented (empty) `Parts` shows all; `{new:{psalm:false}}`
+ hides only the psalm and keeps the rest shown.
- **liturgy cache + sigla**: round-trip the parsed JSON cache; harvest sigla from
fixture pages into a temp TSV and read them back; offline `Load` builds sections
from the TSV and renders the embedded versions with `pl` noted unavailable.