Replace generic "Reading"/"Gospel" headings with real incipits
Deploy / deploy (push) Successful in 1m50s

Bible-plan readings now carry a generated bilingual incipit label
(bible-book-incipits.ts) instead of falling through to the UI's generic
"Reading" heading. Gospel pericopes (whether from the user's own plan
or nocturn-readings.ts's proper content) get the same treatment via a
new label field on the 'gospel' ResolvedPart, recovering the book from
the reading's own citation when no raw book code is available.

Updates 3 matins.test.ts assertions that used "no label" as a stand-in
for "this is a bible-plan reading" — no longer true now that these
readings carry a real label, so they now key off the label's shape
(string = patristic attribution, object = generated incipit) instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
This commit is contained in:
2026-09-03 14:02:58 -04:00
parent 56321ab276
commit 998c549e51
5 changed files with 40 additions and 18 deletions
+6 -2
View File
@@ -92,6 +92,7 @@ import { getSaintRecord } from '../calendar/feasts';
import { getBiblePlanReadings } from '../propers/bible-plan';
import { getNocturnReadings, type NocturnReading } from '../propers/nocturn-readings';
import { getOctaveReading } from '../propers/octave-readings';
import { getGospelIncipitFromCitation } from '../propers/bible-book-incipits';
import { getMatinsSaintOverride, getMatinsCommonOverride, type MatinsPsalmodyScheme } from './matins-psalmody-overrides';
import matinsSundayAntiphonsData from '../data/hours/matins-sunday-antiphons.yml';
import matinsSundayNocturn3OverridesData from '../data/hours/matins-sunday-nocturn3-overrides.yml';
@@ -683,10 +684,12 @@ function nocturnReadingPart(r: NocturnReading): ResolvedPart {
* whereas two adjacent pool entries could be (and, before this, sometimes
* were). */
function gospelReadingPart(r: NocturnReading, homily: NocturnReading | undefined): ResolvedPart {
const incipit = getGospelIncipitFromCitation(r.citation);
return {
kind: 'gospel',
text: { text: r.text, status: r.status, citation: r.citation },
source: r.source,
label: incipit ? { la: incipit.la, en: incipit.en } : undefined,
responsory: r.responsory ? { text: r.responsory, status: { la: 'verified', en: 'verified' } } : undefined,
homily: homily
? { source: homily.source, text: { text: homily.text, status: homily.status, citation: homily.citation } }
@@ -717,10 +720,11 @@ function buildReadingPool(day: LiturgicalDay, temporalId: string, date: string,
// this file's own header) — a bare pericope, still its own 'gospel'
// kind so it gets the same distinguishing UI treatment as a proper
// Gospel+homily.
const label = r.label ? { la: r.label.la ?? '', en: r.label.en ?? '' } : undefined;
parts.push(
r.isGospel
? { kind: 'gospel', text: { text: r.text, status: r.status, citation: r.citation }, responsory }
: { kind: 'lesson', text: { text: r.text, status: r.status, citation: r.citation }, responsory },
? { kind: 'gospel', text: { text: r.text, status: r.status, citation: r.citation }, responsory, label }
: { kind: 'lesson', text: { text: r.text, status: r.status, citation: r.citation }, responsory, label },
);
}
// Grouped by the reading's own `nocturn` tag (ascending), not by which id
+6
View File
@@ -297,6 +297,12 @@ export type ResolvedPart =
text: ResolvedText;
nocturn?: number;
source?: string;
/** The traditional "Léctio sancti Evangélii secúndum..."/"A reading
* from the Holy Gospel according to..." incipit, shown in place of the
* generic "Gospel" heading — see hour-view.ts's renderLessonLabel and
* bible-book-incipits.ts. Absent only when the pericope's own book
* couldn't be identified. */
label?: string | Partial<Record<string, string>>;
responsory?: ResolvedText;
homily?: { source?: string; text: ResolvedText };
};
+10 -2
View File
@@ -20,6 +20,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';
const GOSPEL_BOOKS = new Set(['matt', 'mark', 'luke', 'john']);
@@ -37,6 +38,11 @@ export interface BiblePlanReading {
* data/hours/matins-responsories-by-book.yml — undefined when nothing's
* seeded for that book yet, an honest absence, not a placeholder. */
responsory?: Partial<Record<LanguageCode, string>>;
/** The traditional "Léctio libri..."/"A reading from..." incipit for the
* reading's own (first) book — see bible-book-incipits.ts. Undefined only
* for an abbreviation that map doesn't recognize, which shouldn't happen
* for anything actually under src/data/scripture. */
label?: Partial<Record<LanguageCode, string>>;
}
interface BiblePlanReadingRecord {
@@ -107,15 +113,17 @@ function resolveReading(record: BiblePlanReadingRecord, bookIndex: number): Bibl
la: text.la ? 'verified' : 'missing',
en: text.en ? 'verified' : 'missing',
};
const label = formatCitationLabel(record.passages);
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;
return {
text,
status,
citation: { la: label, en: label },
citation: { la: citationLabelText, en: citationLabelText },
isGospel: isGospelReading(record.passages),
responsory,
label: incipit ? { la: incipit.la, en: incipit.en } : undefined,
};
}
+3 -2
View File
@@ -49,9 +49,10 @@ function renderCitation(text: ResolvedText, languages: readonly string[]): strin
function renderLessonLabel(
label: string | Partial<Record<string, string>> | undefined,
languages: readonly string[],
fallback = 'Reading',
): string {
if (!label) {
return '<h3 class="ordo-part-label">Reading</h3>';
return `<h3 class="ordo-part-label">${escapeHtml(fallback)}</h3>`;
}
if (typeof label === 'string') {
return `<h3 class="ordo-part-label">${escapeHtml(label)}</h3>`;
@@ -115,7 +116,7 @@ function renderPart(part: ResolvedPart, languages: readonly string[]): string {
case 'gospel':
return `
<section class="ordo-part ordo-part-lesson ordo-part-gospel">
<h3 class="ordo-part-label">Gospel</h3>
${renderLessonLabel(part.label, languages, 'Gospel')}
${renderCitation(part.text, languages)}
${renderColumns(part.text, languages)}
${part.source ? `<p class="ordo-part-citation">${escapeHtml(part.source)}</p>` : ''}