diff --git a/TODO.md b/TODO.md index 5eaf390..b5c5ed6 100644 --- a/TODO.md +++ b/TODO.md @@ -5252,3 +5252,33 @@ branch (demoted case) and `getDayLabel`'s final fallback (winning-outright case) in `tests/calendar/ember-days.test.ts` cover both. `npm test` (2024 passed) and `tsc --noEmit` both pass. +**Second follow-on, same session**: user then flagged `/vu/2026-09-18/matins` (Ember Friday, +commemorated) showing the Judith reading under a bare "Reading" heading. Real root cause, and a +much broader bug than just Judith: `data/hours/bible-plan/*.yml`'s ~390-row TSV import uses +English-style book abbreviations (`jdt`, `1kgs`, `luke`, ...) for the ~30 books where that +diverges from this store's canonical Vulgate-numbering form (`jdth`, `3reg`, `luc`, ...) — +`scripture/index.ts`'s own `BOOK_ALIASES` already normalizes this for verse-*text* lookup, but +`bible-plan.ts`'s incipit (`bible-book-incipits.ts`) and responsory (`matins-responsories.ts`) +lookups both used the raw, unaliased abbreviation, silently missing on any canonically-keyed +book and falling back to the generic "Reading" heading. The 3 Gospel aliases (`luke`/`mark`/ +`john`) happened to self-heal through a second, independent citation-regex fallback that only +exists for Gospels (`matins.ts`'s `getGospelIncipitFromCitation`) — every other aliased book +(Judith, both Kings pairs, both Samuel books, Esdras, Osee, Jonas, Josue, Micheas, Abdias, +Judges, the 3 Johannine epistles, James, Jude, Philemon, Titus, Zacharias, Sophonias) had no such +fallback and was silently broken wherever it happened to be the first book of a reading. Fixed by +exporting `canonicalBook` from `scripture/index.ts` (the same substitution `getScriptureChapter` +already applies internally) and routing both `bible-plan.ts` lookups through it — one shared +normalization, not two independently-aliased lookups that can drift again. New test in +`tests/propers/bible-plan.test.ts`. `npm test` (2025 passed) and `tsc --noEmit` both pass. + +Separately, the user also asked about seeing 3 lessons in a row all headed "Pope St. Gregory the +Great, Homily 33 on the Gospels, on Luke 7:36-50" (Ember Friday's own 3-lesson split of one +homily, each with its own distinct text/responsory — see the 2026-08-22 Ember-day entry above). +Confirmed this is correct content, not a duplication bug, but also confirmed it's *not* actually +how the reference source itself labels it: `Tempora/093-5.txt` only carries the attribution once, +on `[Lectio1]` — `[Lectio2]`/`[Lectio3]` have no repeated header at all, continuing silently under +the same nocturn. This app's own authoring convention repeats the full `source` string identically +on every split lesson, and does so consistently across the *entire* nocturn-readings corpus, not +just Ember days — flagged for the user as a real presentational rough edge, not fixed yet pending +their call on scope (a corpus-wide UI/authoring change, not a narrow one-file fix). + diff --git a/src/propers/bible-plan.ts b/src/propers/bible-plan.ts index eaf5144..fc4e031 100644 --- a/src/propers/bible-plan.ts +++ b/src/propers/bible-plan.ts @@ -21,6 +21,7 @@ import type { LanguageCode, TranslationStatus } from '../psalter/types'; import { resolvePassages, type ScriptureCitation } from './octave-readings'; import { getResponsoryForBook } from './matins-responsories'; import { getBookIncipit } from './bible-book-incipits'; +import { canonicalBook } from '../scripture'; const GOSPEL_BOOKS = new Set(['matt', 'mark', 'luke', 'john']); @@ -115,8 +116,9 @@ function resolveReading(record: BiblePlanReadingRecord, bookIndex: number): Bibl }; const citationLabelText = formatCitationLabel(record.passages); const firstBook = record.passages[0]?.book; - const responsory = firstBook ? getResponsoryForBook(firstBook, bookIndex)?.text : undefined; - const incipit = firstBook ? getBookIncipit(firstBook) : undefined; + const canonicalFirstBook = firstBook ? canonicalBook(firstBook) : undefined; + const responsory = canonicalFirstBook ? getResponsoryForBook(canonicalFirstBook, bookIndex)?.text : undefined; + const incipit = canonicalFirstBook ? getBookIncipit(canonicalFirstBook) : undefined; return { text, status, diff --git a/src/scripture/index.ts b/src/scripture/index.ts index bd2e454..ae7395c 100644 --- a/src/scripture/index.ts +++ b/src/scripture/index.ts @@ -58,6 +58,19 @@ export function getScriptureChapter(book: string, chapter: number): ScriptureCha return chapters.get(`${BOOK_ALIASES[book] ?? book}-${chapter}`); } +/** `book` normalized to this store's own canonical abbreviation — the + * same substitution `getScriptureChapter` applies internally, exposed for + * callers (bible-plan.ts's incipit/responsory lookups) that key off a + * *different* store keyed by the canonical form only (bible-book- + * incipits.ts, matins-responsories.ts) and would otherwise silently miss + * on any of `BOOK_ALIASES`' ~30 entries — live case: "jdt" (Judith) + * resolves its verse text fine but fell through bible-book-incipits.ts's + * `jdth`-keyed lookup, rendering as a bare "Reading" heading instead of + * "A reading from the book of Judith" (2026-09-03). */ +export function canonicalBook(book: string): string { + return BOOK_ALIASES[book] ?? book; +} + /** * @param verseRange e.g. "2-19" — inclusive. Whole (authored) chapter when * omitted. Unauthored book/chapter combinations resolve to an empty array diff --git a/tests/propers/bible-plan.test.ts b/tests/propers/bible-plan.test.ts index 6bb61ec..eff917f 100644 --- a/tests/propers/bible-plan.test.ts +++ b/tests/propers/bible-plan.test.ts @@ -17,3 +17,24 @@ describe('getBiblePlanReadings citation labels', () => { expect(readings[2]?.citation.la).toBe('Luke 18:1-23'); }); }); + +// A reading's `label` (the "Léctio libri..." incipit) previously looked up +// bible-book-incipits.ts's canonical-abbreviation-only table using the raw +// book abbreviation from data/hours/bible-plan/*.yml -- which, for ~30 +// books, is one of scripture/index.ts's own English-style aliases (e.g. +// "jdt" for Judith), not the canonical form ("jdth") that table is keyed +// by. The alias resolved verse *text* fine (getScriptureChapter already +// consulted the alias table) but silently missed the label lookup, +// rendering as the generic "Reading" heading instead -- live case: Sept +// 18 2026's own Ember Friday commemoration, `post-pentecost-16-friday`'s +// first reading (Judith 11-13). Fixed 2026-09-03 by routing both the +// incipit and responsory lookups through scripture/index.ts's own +// `canonicalBook`, the same normalization the text lookup already used. +describe('getBiblePlanReadings label uses the same book-alias normalization as verse-text lookup', () => { + it('an aliased book ("jdt" for Judith) still gets its real incipit label, not the generic fallback', () => { + const readings = getBiblePlanReadings('post-pentecost-16', 'friday', '2026-09-18'); + expect(readings[0]?.citation.la).toBe('Jdt 11-13'); + expect(readings[0]?.label?.en).toBe('A reading from the book of Judith'); + expect(readings[0]?.label?.la).toBe('Léctio libri Judith'); + }); +});