aboutsummaryrefslogtreecommitdiff
path: root/docs/superpowers/specs/2026-07-23-lectio-go-rewrite-design.md
blob: f0889e34bcb3234b42f48d6d9fd7bf25dcac1a15 (plain) (blame)
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# lectio — design spec

Date: 2026-07-23
Status: approved (brainstorm), pre-plan

## Goal

Reimplement the Python `daily-reading` tool (`ewangelia.py`) as a self-contained
Go project with two binaries:

- **`lectio`** — terminal CLI (subcommand-based).
- **`lectio-ui`** — TUI reader (Bubble Tea), modelled on the user's `bread-calc`.

It fetches the daily Catholic liturgy readings from niezbednik.niedziela.pl and
shows them in Polish plus four other versions (Wujek Polish, Vulgate Latin,
Greek, Douay-Rheims English), with correct psalm versification across them.

This is a **fork**: the Python `daily-reading` project is left untouched and
keeps working. `lectio` is a fresh project.

## Non-goals

- **Computing** the day's citations from a liturgical-calendar engine (for dates
  the site never published, or with no network and no prior harvest) remains the
  separate `offline_readings` effort. `lectio` gets citations from the site, or
  from a prior `lectio update` harvest — it does not compute them. When an
  offline engine exists it slots in behind `internal/liturgy` unchanged.
- No new translations beyond the five already supported.

Note: harvesting the site's **published** future sigla (`lectio update`) and
reading them offline *is* in scope — it is the "harvest" path, distinct from
calendar computation.

## Key decisions

| Decision | Choice |
|---|---|
| Language / stack | Go; Bubble Tea + Lipgloss (TUI), go-toml/v2 (config). Match bread-calc. |
| Binaries | `lectio` (CLI), `lectio-ui` (TUI). |
| Self-contained | Embed all four Bible corpora via `go:embed`; verse lookup in Go. No external `vul`/`grb`/`wuj`/`drb` tools. |
| Daily data | Still fetched from niedziela.pl (source of citations + Polish text); cached on disk. |
| CLI shape | Subcommands (hand-rolled dispatch, no cobra). |
| TUI shape | Reader + version-switch: full day in one pane, `tab` cycles versions, arrows change date. |
| Config | `~/.config/lectio/config.toml`, auto-seeded, flags override. |
| Versification | Port `psalm_versify.py` behaviour exactly. |

## Project layout

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/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)
internal/config/          TOML config load + auto-seed
internal/cli/             subcommand dispatch
internal/tui/             Bubble Tea reader
Makefile, README.md, LICENSE, go.mod
```

Each `internal/*` package has one clear purpose, a small exported surface, and
is unit-testable in isolation. CLI and TUI are thin front-ends over the same
core (liturgy + bible + psalter + render).

## Data architecture

### Embedded corpora
`internal/bible` embeds four TSV files with `go:embed`:

- `wuj.tsv` — Biblia Wujka (Polish), from `offline_readings/corpus/wuj.tsv`.
- `drb.tsv` — Douay-Rheims (English), from `offline_readings/drb/drb.tsv`.
- `vul.tsv` — Vulgate (Latin), extracted from the installed `vul` tool
  (`sed '1,/^#EOF$/d' $(command -v vul) | tar xzf - -O vul.tsv`).
- `grb.tsv` — Greek, extracted from the installed `grb` tool the same way.

All four share the 6-column format `Book | Abbrev | BookNum | Chapter | Verse | Text`
(the kjv-family format). Total ~15 MB, embedded into the binary.

### Reference lookup (replaces the awk engine + shell-out)
`internal/bible` reimplements the reference grammar in Go. It must support the
subset the tool actually feeds it:

- Book match: canonical English name (`John`), English prefix (`Joh`), and the
  Polish/English alias table (`J`, `Łk`, `1 Kor`, `Jana`, `Mdr`, ...) resolved
  to a canonical book. Longest alias wins; exact before prefix (fixing the old
  `J`→Joshua bug).
- Chapter+verse forms: `Book C:V`, `Book C:V-V` (range), `Book C:V,V,...`
  (list). Mixed lists like `John 20:1,11-18` are split into single-group
  queries and merged (port of `split_ref`).
- Returns `[]Verse{Chapter, Verse, Text}` in reference order; reports the groups
  a corpus lacked (for the "brak w …" note).

### Daily readings
`internal/liturgy`:

- `Fetch(date, refresh) (html, error)` — GET
  `https://niezbednik.niedziela.pl/liturgia/{date}/Ewangelia` with a browser
  User-Agent. Cache only fully-published pages.
- `Parse(html) ([]Section, error)` — pick the **new** lectionary tab
  (`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}`.

### Caching (fast repeat loads)
Two layers under `~/.cache/lectio/` (honor `XDG_CACHE_HOME`):

- Raw HTML `{date}.html` — skips the network re-fetch of a published page.
- Parsed sections `{date}.json` — skips re-parsing; a repeat load the same day
  hits this and is near-instant (no network, no parse).

Only fully-published pages are cached (an unpublished future date keeps being
retried, never cached as "empty"). `--refresh` bypasses both layers.
`Load(date)` tries JSON → HTML(+parse, write JSON) → fetch(+cache both).

### Sigla harvest (`lectio update`) and offline use
The site publishes each day's reading **sigla** (the scripture citations) weeks
to months ahead. `lectio update` walks forward from today, fetching each date
and extracting its citations, until it reaches the unpublished horizon (a page
with no readings). It writes them to a persistent TSV:

    ~/.local/share/lectio/sigla.tsv     (honor XDG_DATA_HOME)

One row per reading section: `date <TAB> section_label <TAB> citation`, e.g.

    2026-07-22    1. czytanie    Pnp 8, 6-7
    2026-07-22    Psalm          Ps 63 (62), 2. 3-4. 5-6. 8-9 (R.: por. 2ab)
    2026-07-22    Ewangelia      J 20, 1. 11-18

`update` merges with the existing file (re-harvesting a date replaces its rows),
warms the HTML/JSON cache for those dates, and reports how many days it added and
the furthest date reached.

**Offline use.** When offline (config `offline = true`, the `--offline` flag, or
an automatic fallback after a failed fetch), `Load` reads the sigla TSV instead
of the network. If the date is present it builds the sections from the stored
citations and renders the four **embedded** Bible versions from the corpora — so
after one `lectio update`, any harvested day reads fully offline in Wujek Polish,
Latin, Greek and Douay-Rheims. The `pl` version (modern niedziela.pl / Biblia
Tysiąclecia) is online-only and not embedded (copyright); **offline, `wuj`
(Wujek) takes the Polish role** — `pl` is dropped from any version list and
replaced by `wuj` where needed (see Config → `offline`). A date absent from the
harvest gives a clear "run `lectio update` online" error.

## Versions and psalm systems

Ported from `ewangelia.py` constants:

```
versions:   pl, wuj, vul, grb, drb   (canonical order; = config `versions` default)
labels:     pl="Polski (niedziela.pl)"  wuj="Wujek (pol.)"  vul="Wulgata (lac.)"
            grb="Grecki"               drb="Douay-Rheims (ang.)"
bible tools: wuj, vul, grb, drb   (pl comes from the fetched paragraphs)
psalm system: vul/grb/wuj -> "vulgate"   drb -> "drb"
```

`lectio show VERSION` accepts any of the five (pl renders the fetched paragraphs;
the others render looked-up verses).

`pl` renders the fetched Polish paragraphs (dropping the "Słowa Ewangelii"
incipit, and de-duplicating a repeated responsorial refrain). The four bible
versions render verses looked up from the embedded corpora, with the citation
converted per that version's psalm system.

## Reference conversion & psalm versification

`internal/bible` (conversion) + `internal/psalter` (versification) port
`to_english_ref` / `_psalm_ref` / `psalm_versify.py`:

- Strip a leading `por.` (compare marker) and a trailing `(R.: ...)`
  responsorial refrain from the citation.
- Convert the Polish citation to English style and normalise verse groups
  (`Mt 7, 1-5` -> `Mat 7:1-5`; disjoint groups preserved as a comma list).
- Psalms: the lectionary cites `Ps H (V)`. The Vulgate versions use chapter `V`,
  verses unchanged. `drb` uses chapter `H` and shifts each verse by the
  title-fold count `k` for that psalm: `drb_verse = lectionary_verse - k`
  (clamped ≥ 1). `k` comes from the `DRB_TITLE_FOLD` table (1 or 2 title lines;
  0 for untitled), derived by aligning the Wujek/DRB corpora. ~140 psalms exact,
  ~10 approximate (documented in the package).

## CLI surface (`internal/cli`)

Hand-rolled subcommand dispatch. `lectio` with no args = `lectio today`.

```
lectio today   [--all] [--raw] [--width N] [--refresh]
lectio date D  [--all] [--raw] [--width N] [--refresh]        # D = YYYY-MM-DD
lectio compare LIST [--date D] [--all] [--width N] [--refresh]
      LIST = comma versions (default: config `versions`)
lectio show VERSION [--date D] [--all] [--refresh]            # one version's text
lectio update [--days N] [--from D]     # harvest future sigla to the TSV
lectio --version | lectio -v
lectio help | lectio -h | lectio <cmd> -h
```

- `--all` shows every reading (1st/2nd, psalm, acclamation, gospel); default is
  the gospel only, unless config `all = true`.
- `--raw` drops banner/headings for piping.
- `--refresh` bypasses the cache and re-fetches.
- `--offline` (global) skips the network and uses the sigla TSV; also applied
  automatically when a fetch fails.
- `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
  usage error.

## TUI (`internal/tui`)

Bubble Tea Elm architecture, per the approved mockup (reader + version-switch):

- One scrolling pane showing the whole day's readings for the **active version**.
- Header: date + active version label. Footer: keybar.
- Keys: `tab`/`shift+tab` cycle versions (order = config `versions`, starting at
  `default_version`); `←/→` change date (async re-fetch with a loading state);
  `j/k` and `space`/`b` scroll; `g/G` top/bottom; `r` refresh; `q`/`ctrl+c` quit.
- Offline (config/flag): the version cycle omits `pl` (uses `wuj` for Polish);
  dates come from the harvest; a header hint shows the offline state.
- Async fetch via `tea.Cmd` returning a `readingsMsg` or `errMsg`; a spinner or
  "ładowanie…" line while in flight.
- Lipgloss styling, theme-neutral (works on light/dark terminals); width-aware
  wrapping from `tea.WindowSizeMsg`.
- `Model{ config, cache, date, version, sections, scroll, width, loading, err }`.

The TUI shows one version at a time (not columns); the CLI `compare` is where
side-by-side lives.

### Colour scheme
Colours distinguish the parts of a reading. Each role is a named Lipgloss style
in one place (`internal/tui`), using `lipgloss.AdaptiveColor` so it reads on both
light and dark terminals; a `NO_COLOR` env / non-TTY output degrades to plain.

| Role | Style |
|---|---|
| Section heading (`1. czytanie`, `Ewangelia`) | bold, accent colour |
| Citation (`Pnp 8, 6-7`) | dim / muted, next to the heading |
| Verse number (`20:1`) | distinct muted colour, separated from text |
| Verse text | default foreground |
| Responsorial refrain (psalm) | italic / secondary colour |
| Header (date + active version) | accent background or bold |
| Footer keybar | dim |

Exact hues are finalised in planning, but the role → style mapping above is
fixed. The CLI stays plain text (colour is a TUI concern).

## Config (`internal/config`)

`Config` struct loaded via go-toml. Resolution: `LECTIO_CONFIG` env →
`~/.config/lectio/config.toml` (auto-seeded from an embedded default on first
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
```

Unknown versions in config are rejected with a clear error. Invalid TOML falls
back to defaults with a stderr warning.

`offline = true` (or the `--offline` flag) makes lectio work purely from the
`lectio update` harvest and cache — it never touches the network, and `pl`
(online-only) is dropped in favour of `wuj` as the Polish version:

- Any version list drops `pl`; if the list had `pl` but not `wuj`, `wuj` takes
  its place. So default `versions` become `["wuj","vul","grb","drb"]` offline.
- `default_version`/`show pl` fall back to `wuj` offline.
- A date not present in the sigla harvest gives a clear "not harvested; run
  `lectio update` online" error.

Seeded default is `offline = false` (a fresh install has no harvest yet); a user
who runs `lectio update` on a schedule can flip it to `true` for a fast,
network-free daily read.

## Porting map (Python → Go)

| Python (`ewangelia.py` / `psalm_versify.py`) | Go |
|---|---|
| `fetch`, `parse_sections`, `html_to_lines`, `extract_reference` | `internal/liturgy` |
| awk engine, `run_bible_tool`, `split_ref` | `internal/bible` (lookup) |
| `POLISH_TO_EN`, `to_english_ref`, `_psalm_ref` | `internal/bible` (aliases + conversion) |
| `psalm_versify.py` (`DRB_TITLE_FOLD`, `drb_verse`, chapter map) | `internal/psalter` |
| `gather_version`, `run_bible_section`, `render_compare`, `render_section`, refrain dedup | `internal/render` |
| `VERSION_LABELS`, `PSALM_SYSTEM`, `COMPARE_CODES`, `BIBLE_TOOLS` | `internal/bible` / `internal/render` constants |
| `argparse` main | `internal/cli` |

## Error handling

- Network failure: fall back to the sigla TSV (offline mode) if the date is
  harvested — render the four embedded versions, note that `pl` is unavailable.
  If the date is not harvested either, clear stderr message, exit 1 (CLI) or an
  error line in the TUI (stay usable, let the user change date).
- Unpublished future date: "no reading published for this date yet", exit 1;
  `update` treats it as the horizon and stops.
- Layout change (tab missing/empty): explicit "site layout may have changed"
  error, exit 1.
- A corpus lacking a passage (e.g. deuterocanonical book absent from a version):
  per-section note, not a crash; other versions still render.
- Unknown version code: usage error, exit 2.

## Testing

- **bible**: table tests for book matching (incl. `J`→John, `Łk`→Luke, `1 Kor`),
  reference grammar (ranges, lists, split groups), and known verses across all
  four corpora (Gen 1:1, John 20:1, a deuterocanonical, a psalm).
- **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.
- **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.
- **render**: golden-text tests for a compare block and a section, including the
  refrain dedup and psalm-number divergence.
- **config**: load/seed/override tests with a temp `XDG_CONFIG_HOME`.
- `go vet ./...` clean; `gofmt`-formatted.

## Build & install

Makefile cloned from bread-calc:

```
make build     -> ./lectio and ./lectio-ui
make install   -> $(PREFIX)/bin (default ~/.local/bin)
make cross     -> dist/ for linux/darwin/windows amd64+arm64
make test | vet | fmt | clean
```

`.gitignore`: `/lectio`, `/lectio-ui`, `dist/`, `*.test`, coverage, cache.

## Open items (decide during planning)

- Exact Lipgloss hues for the colour roles (mapping is fixed in the TUI section;
  only the specific colours are open).
- Whether `lectio show` and the TUI share a single "gather one version" function
  (they should).
- Whether to vendor the extracted `vul.tsv`/`grb.tsv` into the repo or fetch them
  at build time (lean: vendor them under `internal/bible/`, like wuj/drb).