# National Bibles Implementation Design **Goal:** Let anyone add their nation's Bible to lectio as a formatted data file — usable either dropped into a runtime directory (no toolchain) or embedded into the shipped binary via a validated Makefile step — so offline readings can render in any language, and so a corrected Wujek can return as a pure data drop-in. **Architecture:** A "corpus" is a pair of files sharing a basename ``: `.tsv` (verse text) + `.ini` (metadata). The identical layout serves both built-in corpora (in `internal/bible/corpora/`, embedded via a glob `go:embed`) and user corpora (in a runtime directory, discovered at startup, overriding built-ins of the same code). One validator (`lectio --corpus-check`) gates both paths. Reading-text selection becomes config-driven (`reading_version` explicit, else language auto-match, else Latin fallback). No third-party dependencies; pure stdlib + the existing `internal/ini` reader. **Tech Stack:** Go (stdlib + `internal/ini`), 6-column TSV corpora, INI sidecars, `go:embed`, Makefile + POSIX `sh` scripts. ## Global Constraints - **Corpus text format:** 6 tab-separated columns, exactly, UTF-8, one row per verse: `Book · Abbrev · BookNum · Chapter · Verse · Text`. Rows whose column count != 6 are skipped by the loader (existing behaviour, unchanged). - **`Book` column:** the canonical English book name, and it MUST be a member of the canonical book set (the 73 keys in `internal/bible/books.ini`). Lookups key on this column; `Abbrev` (col 2) and `BookNum` (col 3) are informational and ignored by the loader. - **Metadata sidecar:** `.ini`, full-line-comment INI (parsed by `internal/ini`), keys: `lang` (required), `name` (required), `psalm_system` (required: `vulgate`|`hebrew`|`drb`), `sigla` (optional, a `books.ini` dialect id). - **Delivery:** both compile-in (glob `go:embed` + Makefile validate/embed) and runtime drop-in (a user directory, overrides built-ins). Same format and validator for both. - **User corpus directory:** `~/.config/lectio/corpora/` (consistent with `~/.config/lectio/calendars/`; see Open Decision D1). - **Selection order (offline reading text):** `reading_version` (explicit corpus code) → corpus whose `lang` matches `reading_lang` → corpus whose `lang` matches `ui_language` → Latin `vul` fallback. Defaults preserved via the `ui_language` step (en→drb; pl→no pl corpus→vul). - **`reading_lang` is the reading-language knob:** a new config field, any language code (`fr`, `de`, `pl`, `en`, …), default empty. When set it auto-selects a corpus of that language; when empty the reading language follows `ui_language`. - **`traditional_lang` is not read by the corpus resolver at all and is deprecated:** it stays `pl`/`en`, keeps its sole role as the missalemeum-scraper language knob for the still-live daily view, gains no new semantics, and `reading_lang` now owns the reading-language role it used to imply. It is removed only when the scraper is retired (separate future work). - **Purity:** `internal/bible` imports only stdlib + `internal/ini` + `internal/psalter`. No network, no new third-party deps. - **Non-breaking:** existing `--ref`, version comparison, TUI/web daily view, and the scraper path keep working unchanged. ## Detailed Design ### 1. Corpus file pair A corpus `fr-crampon` is two files: `fr-crampon.tsv` (verse text; `Abbrev`/`BookNum` may be placeholders): ``` Genesis Gn 1 1 1 Au commencement, Dieu créa le ciel et la terre. Genesis Gn 1 1 2 La terre était informe et vide… Daniel Dn 27 13 1 Il y avait à Babylone un homme du nom de Joakim. ``` `fr-crampon.ini` (metadata): ```ini ; lectio corpus metadata lang = fr name = Bible Crampon (français) psalm_system = vulgate sigla = fr ``` `psalm_system` values: - `vulgate` — Septuagint/Vulgate psalm numbering (e.g. Ps 9 = Hebrew 9+10). Used by `vul`, `wuj`, `grb`. - `hebrew` — Masoretic/modern numbering, no verse shift. - `drb` — Hebrew chapter numbering with the Douay-Rheims title-fold verse shift. Used by `drb`. Built-in corpora gain their own sidecars (`vul.ini`, `drb.ini`, `grb.ini`, `wuj.ini`) so the metadata is uniform and no version metadata is hard-coded in Go. ### 2. Discovery & loading - **Embed:** change the directive in `internal/bible/bible.go` from the explicit file list to a glob: `//go:embed corpora/*.tsv corpora/*.ini`. Adding a pair to `corpora/` + rebuild embeds it — no directive edit. - **Runtime:** `~/.config/lectio/corpora/` is scanned once at first corpus use. A `.tsv` there defines/overrides code ``; its sidecar `.ini` supplies metadata (falling back to the embedded sidecar if the user omitted one for an overridden built-in). - **Override rule:** for a given code, a runtime file wins over the embedded file (so a corrected `wuj.tsv` replaces the broken built-in). New codes simply add to the registry. - **Registry:** a process-wide map `code → {source: embed|user, meta}` built lazily and cached under the existing `corporaMu`. `load(code)` reads the winning `.tsv` into the existing `books[book][chap][]Verse` structure (verse order preserved from file order, as today). ### 3. Corpus metadata (`internal/bible/corpusmeta.go`, new) - `type CorpusMeta struct { Code, Lang, Name, PsalmSystem, Sigla string }`. - `Corpora() []CorpusMeta` — all known corpora (embedded + user), sorted, user overriding embed. - `Meta(code string) (CorpusMeta, bool)`. - `CorporaForLang(lang string) []CorpusMeta` — for selection. - Parsing uses `ini.Parse`; a missing sidecar for a user `.tsv` yields a `CorpusMeta` with empty fields (validator flags it; loader still serves text with `psalm_system` defaulting to `vulgate` and a warning). ### 4. Selection & fallback - `config.Config` gains two fields, both default empty: `ReadingLang string` (INI key `reading_lang`, any language code) and `ReadingVersion string` (INI key `reading_version`, an explicit corpus code). - New `config.Config.ReadingCorpus() string`: 1. if `ReadingVersion` is set and that corpus exists → it; 2. else, for each candidate language in order [`ReadingLang`, `NormalizeUILanguage(UILanguage)`] that is non-empty, the first corpus (deterministic order) whose `lang` equals that language → that corpus; 3. else `""`. - `internal/cli/liturgy.go` `vernacularVersion(cfg)` becomes `code := cfg.ReadingCorpus(); if code == "" { code = "vul" }; return code`. The old special-case defaults collapse into step 2: `drb` is selected for English via its own `lang = en` metadata, and Polish (no `pl` corpus) falls through to the Latin `vul`. - The existing `latinFallback = "vul"` mechanism in `readingLine` is unchanged and still covers per-verse gaps in whatever corpus is chosen. - **Behaviour:** `reading_lang = fr` auto-selects a `lang = fr` corpus — any language, not just pl/en. Leaving `reading_lang` empty makes the reading language follow `ui_language`. A dropped-in corpus declaring `lang = pl` (e.g. a corrected Wujek) auto-serves Polish users with **zero config** as soon as `reading_lang` or `ui_language` is `pl`, superseding the `vul` fallback. `traditional_lang` is never consulted. ### 5. Psalm versification (metadata now; application deferred) Each corpus declares `psalm_system` in its sidecar, and the validator checks the value is one of `vulgate`/`hebrew`/`drb`. Actual *application* — mapping a psalm citation's numbering to the corpus's system at render time — is **deferred**: the offline lectionary currently carries only Epistle + Gospel (no psalm/gradual/tract parts), the offline render path (`readingLine` → `bible.Lookup`) does not run the `convert.go` psalm mapping at all, so building per-corpus psalm mapping now would be dead code (YAGNI). The metadata is captured so corpora are fully described and the hazard is recorded; the numbering application lands with the future psalm-readings work, which revisits `internal/bible/convert.go`'s `psalmRef` system switch (today `vulgate`/`drb`, to gain `hebrew`). This is where the concrete `wuj` Hebrew-vs-Vulgate mismatch gets resolved. ### 6. Validation — `lectio --corpus-check ` Runs against a code resolvable from either the embed or the user dir (also accepts a path for pre-embed CI use). Reports: - **Parse:** every non-blank line has exactly 6 tab-separated fields; `Chapter`/`Verse` parse as positive ints; text is valid UTF-8 with no embedded tab/newline. - **Book names:** every `Book` value is in the canonical set (the `books.ini` keys); lists unknown names. - **Verse integrity, per (book, chapter):** duplicate verse numbers; non-monotonic order; intra-chapter gaps. - **Sidecar:** `.ini` present; `lang`/`name`/`psalm_system` set; `psalm_system` ∈ {vulgate,hebrew,drb}; `sigla` (if set) names an existing `books.ini` dialect. - **Coverage report vs Latin `vul`:** missing books, missing whole chapters, and — classified by run-shape — contiguous blocks (likely real omissions) vs scattered single verses (likely different division), plus a psalm-count flag. (This is the audit already prototyped in this session, promoted to a command.) - **Exit code:** non-zero on hard errors (bad format, unknown book, bad sidecar); zero with warnings on coverage gaps. Human-readable text output; a `--json` flag emits the same as JSON for CI. ### 7. Scripts & Makefile - `scripts/corpus-validate.sh ` — thin wrapper: `go run ./cmd/lectio --corpus-check "$1"`; usable standalone (contributor / CI) before embedding. Non-zero exit propagates. - `Makefile` targets: - `add-corpus CORPUS= SRC=` — validates the pair at `SRC/.{tsv,ini}`, and only on success copies both into `internal/bible/corpora/`, then runs `build`. Validation failure aborts before any copy (so a broken Bible can never be embedded). - `check-corpora` — runs `--corpus-check` over every embedded `` (CI gate; also wired into the default `test`/`check` target). - The runtime drop-in path needs none of these — drop the pair into `~/.config/lectio/corpora/` and run. ### 8. Surfacing user corpora - `lectio --list` and the version-comparison surfaces (CLI/TUI/web) enumerate corpora via `bible.Corpora()` and label them with each corpus's `name`, so a user corpus appears wherever built-ins do. - i18n `Version` labels fall back to the corpus `name` for codes not in the static i18n map. ## Testing Approach - `internal/bible`: table tests for glob-embed loading, user-dir override precedence, metadata parsing (valid/missing/malformed sidecar), and `ReadingCorpus` resolution order. - `internal/config`: `reading_lang` and `reading_version` round-trip through INI read/render; `ReadingCorpus()` resolution order (explicit version, reading_lang match, ui_language match, empty). - `internal/cli`: `--corpus-check` on fixtures — a clean corpus, one with an unknown book, one with a verse gap, one missing a sidecar, one with a bad `psalm_system` value — asserting messages and exit codes; `--json` shape. - Makefile: `check-corpora` passes for all shipped corpora (guards regressions in the built-in sidecars). - Fixtures live under `internal/bible/testdata/corpora/`, tiny (a few books), not full bibles. ## Out of Scope (YAGNI) - Shipping any actual national Bible (each is a later per-contribution data drop-in, including a corrected Wujek). - Retiring the missalemeum scraper and removing `traditional_lang` (separate "migrate daily view off scraper" work; gated on deciding how much of the full EF propers to compute offline). - Computing non-reading EF propers (Introit/Collect/Gradual/Tract/Offertory/Secret/Communion/Postcommunion). - Auto-download of bibles, any GUI for corpus management, per-form (OF vs EF) corpus selection (the vernacular text is form-independent). ## Open Decisions - **D1 — user corpus directory:** chosen `~/.config/lectio/corpora/` for consistency with `~/.config/lectio/calendars/` and single-place discoverability. Alternative: `~/.local/share/lectio/corpora/` (XDG-correct for multi-MB data). Revisit if the config-dir size bothers you. - **D2 — `sigla` linkage:** a corpus may name a `books.ini` dialect for citation *display*; citation *resolution* stays English-authored (unchanged). If a corpus's language has no `books.ini` dialect, display falls back to the configured `sigla_style`. No new dialect is required to add a corpus.