2cc856ed92
Replaces the fixed Nocturn 1 = plan / Nocturns 2-3 = patristic slotting with one ordered pool (scripture plan, winner/commemorations/temporal id, active octaves) sliced evenly across however many nocturns the day has. Also makes the bible-plan store dual-keyed so the Dec 25 - Jan 13 stretch can key off a fixed calendar date alongside the usual (temporalId, weekday) key. Mechanism only — content authoring is still 2 proof dates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
129 lines
6.0 KiB
TypeScript
129 lines
6.0 KiB
TypeScript
// The user's own continuous scripture-reading plan, one contributor among
|
|
// several to Matins' pooled reading list (see hours/matins.ts's header and
|
|
// TODO.md's Matins section) — deliberately not the historical per-day
|
|
// lectionary. Most rows key off the same (temporalId, weekday) pair every
|
|
// other content store in this project already uses to identify a day, but
|
|
// the source TSV's Christmastide/Epiphanytide stretch (Dec 25 - Jan 13)
|
|
// keys off a fixed calendar date (MM-DD) instead, since that stretch is
|
|
// read straight through regardless of which temporal Sunday governs the
|
|
// day. The two key types can both apply to the same date (e.g. a Sunday
|
|
// after Epiphany landing inside Jan 1-13) — `getBiblePlanReadings` checks
|
|
// both and pools whatever each produces, rather than one overriding the
|
|
// other.
|
|
//
|
|
// Only a small, growable subset is authored so far — this is the mechanism
|
|
// build, not the full-calendar content pass (~390 rows total in the
|
|
// source TSV); see TODO.md for what's deferred. Every entry not yet
|
|
// authored here simply resolves to no readings (honest absence, not a
|
|
// placeholder) — matins.ts's reading pool just has nothing from this
|
|
// source that day.
|
|
import type { LanguageCode, TranslationStatus } from '../psalter/types';
|
|
import { resolvePassages, type ScriptureCitation } from './octave-readings';
|
|
import { getResponsoryForBook } from './matins-responsories';
|
|
|
|
const GOSPEL_BOOKS = new Set(['matt', 'mark', 'luke', 'john']);
|
|
|
|
export interface BiblePlanReading {
|
|
text: Partial<Record<LanguageCode, string>>;
|
|
status: Partial<Record<LanguageCode, TranslationStatus>>;
|
|
citation: Partial<Record<LanguageCode, string>>;
|
|
/** True when every passage in this reading is from one of the four
|
|
* Gospels — the user's own plan deliberately never assigns one on a
|
|
* Sunday (confirmed 2026-08, not a gap) — see hours/matins.ts for how
|
|
* this combines with the day's own proper Gospel+homily, when one
|
|
* exists, rather than replacing it. */
|
|
isGospel: boolean;
|
|
/** Matched loosely by the reading's own (first) book against
|
|
* 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>>;
|
|
}
|
|
|
|
interface BiblePlanReadingRecord {
|
|
passages: ScriptureCitation[];
|
|
/** Set explicitly when the cited passages aren't in src/data/scripture
|
|
* yet (the common case until the full Vulgate/Douay-Rheims import
|
|
* lands) — mirrors octave-readings.ts's own convention rather than
|
|
* inferring "missing" from empty resolved text, since a genuinely short
|
|
* authored passage could otherwise look indistinguishable from an
|
|
* unauthored one. */
|
|
status?: Partial<Record<LanguageCode, TranslationStatus>>;
|
|
}
|
|
|
|
interface BiblePlanDayRecord {
|
|
/** Mutually exclusive with `calendarDate` — a row keys off one or the
|
|
* other, never both (see this file's header). */
|
|
temporalId?: string;
|
|
weekday?: string;
|
|
/** "MM-DD", for the Dec 25 - Jan 13 stretch that reads straight through
|
|
* regardless of which temporal Sunday governs the day. */
|
|
calendarDate?: string;
|
|
readings: BiblePlanReadingRecord[];
|
|
}
|
|
|
|
function citationLabel(citation: ScriptureCitation): string {
|
|
const book = citation.book.charAt(0).toUpperCase() + citation.book.slice(1);
|
|
return citation.verses ? `${book} ${citation.chapter}:${citation.verses}` : `${book} ${citation.chapter}`;
|
|
}
|
|
|
|
function isGospelReading(passages: ScriptureCitation[]): boolean {
|
|
return passages.every((p) => GOSPEL_BOOKS.has(p.book));
|
|
}
|
|
|
|
function resolveReading(record: BiblePlanReadingRecord, bookIndex: number): BiblePlanReading {
|
|
const text = resolvePassages(record.passages);
|
|
const status = record.status ?? {
|
|
la: text.la ? 'verified' : 'missing',
|
|
en: text.en ? 'verified' : 'missing',
|
|
};
|
|
const label = record.passages.map(citationLabel).join('; ');
|
|
const firstBook = record.passages[0]?.book;
|
|
const responsory = firstBook ? getResponsoryForBook(firstBook, bookIndex)?.text : undefined;
|
|
return {
|
|
text,
|
|
status,
|
|
citation: { la: label, en: label },
|
|
isGospel: isGospelReading(record.passages),
|
|
responsory,
|
|
};
|
|
}
|
|
|
|
const modules = import.meta.glob<{ default: BiblePlanDayRecord }>('../data/hours/bible-plan/*.yml', {
|
|
eager: true,
|
|
});
|
|
|
|
const readingsByTemporalKey = new Map<string, BiblePlanReading[]>();
|
|
const readingsByCalendarDate = new Map<string, BiblePlanReading[]>();
|
|
for (const mod of Object.values(modules)) {
|
|
const { temporalId, weekday, calendarDate, readings } = mod.default;
|
|
// Cycles independently per book (not per reading) so two same-day
|
|
// readings from the same book don't collide on the pool's first entry —
|
|
// see matins-responsories.ts's own cycling doc comment.
|
|
const seenPerBook = new Map<string, number>();
|
|
const resolved = readings.map((record) => {
|
|
const book = record.passages[0]?.book ?? '';
|
|
const index = seenPerBook.get(book) ?? 0;
|
|
seenPerBook.set(book, index + 1);
|
|
return resolveReading(record, index);
|
|
});
|
|
if (calendarDate) {
|
|
readingsByCalendarDate.set(calendarDate, resolved);
|
|
} else {
|
|
readingsByTemporalKey.set(`${temporalId}-${weekday}`, resolved);
|
|
}
|
|
}
|
|
|
|
/** Every reading this plan contributes to Matins' pooled reading list for
|
|
* `date` — both the (temporalId, weekday) row and the fixed-calendar-date
|
|
* row (if any), pooled together rather than one overriding the other, since
|
|
* both can genuinely apply to the same date (see this file's header).
|
|
* Empty array, not undefined, when nothing's authored for this day yet —
|
|
* every caller already treats "no readings" as a valid, renderable state
|
|
* (see hours/matins.ts), so there's no separate "missing entirely" signal
|
|
* to preserve here the way getOctaveReading needs `undefined` for. */
|
|
export function getBiblePlanReadings(temporalId: string, weekday: string, date: string): BiblePlanReading[] {
|
|
const byTemporal = readingsByTemporalKey.get(`${temporalId}-${weekday}`) ?? [];
|
|
const byCalendarDate = readingsByCalendarDate.get(date.slice(5)) ?? [];
|
|
return [...byTemporal, ...byCalendarDate];
|
|
}
|