Build the Vespers ordo: mechanism, weekday content, seasonal content
Wires resolveOrdo('vespers', ...) end to end: opening versicle,
weekday psalmody (4 groups, handling the joined Ps 115+116 and the
split Ps 138/143 via a PsalmRef-like shape), the chapter/responsory/
hymn/versicle office bundle, Magnificat, day-collects — reusing
resolveEveningDay so First/Second Vespers precedence and the cross-day
commemoration (both added in the previous commit) apply automatically
— and a closing that reuses Lauds' own conclusio, since no distinct
"Vespera" version of that exact text turned up in the source.
getMagnificatAntiphon mirrors getBenedictusAntiphon's saint/temporal-id
lookup order, but also falls back to real content Benedictus has no
equivalent of: the classic ferial set that quotes the Magnificat's own
text in sequence across the week, with the correct Monastic-rubric
variant picked for Tue/Thu/Fri/Sat (two of those needed a fresh
translation where the Monastic Latin diverges from the given English
in content, not just wording).
Also includes Vespers' own seasonal content (Advent/Lent/Passiontide/
Paschaltide chapter+responsory+hymn+versicle), transcribed from the
same source blocks as the weekday content, with the Monastic-specific
hymn variants hand-applied from the source's substitution notation
against each base Roman text (one transcription slip caught and fixed
mid-pass: the Passiontide hymn's closing lines had been copied from
the Roman text instead of the Monastic one actually being
transcribed). Passiontide's own responsory/hymn correctly omit the
Gloria Patri, matching real rubric and the source data as given.
All weekday/seasonal content marked `draft`, not `verified` — read
directly from the reference source, not cross-checked against a live
rendering per day the way the psalmody was.
Six other hours' resolvePart switches needed the three new HourPart
kinds ('vespers-psalmody', 'vespers-office', 'magnificat') added to
their exhaustive not-used lists so they still typecheck; three
pre-existing tests assumed Vespers was still "coming soon" and are
updated to exercise Matins (still the genuine stub) instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,23 @@
|
||||
import type { ResolvedPart, ResolvedText } from './types';
|
||||
import type { Commemoration, DayWinner, LiturgicalDay } from '../calendar/types';
|
||||
import type { Commemoration, DayWinner, LiturgicalDay, Weekday } from '../calendar/types';
|
||||
import type { ProperText } from '../propers';
|
||||
import { getCommonProper, getTemporalProper } from '../propers';
|
||||
import { getSaintRecord } from '../calendar/feasts';
|
||||
import { resolveActiveOctave } from '../calendar';
|
||||
import { splitAntiphon } from './antiphon';
|
||||
import vespersMagnificatAntiphonsData from '../data/hours/vespers-magnificat-antiphons.yml';
|
||||
|
||||
type BilingualText = Partial<Record<string, string>>;
|
||||
type TranslationStatus = 'verified' | 'draft' | 'missing';
|
||||
interface MagnificatWeekdayDefault {
|
||||
antiphon: BilingualText;
|
||||
status?: Partial<Record<string, TranslationStatus>>;
|
||||
}
|
||||
// Monday-Saturday only — Sunday has no fixed weekday default (its own
|
||||
// antiphon varies by Proper of the Time; see the data file's own header).
|
||||
const vespersMagnificatAntiphons = vespersMagnificatAntiphonsData as Partial<
|
||||
Record<Weekday, MagnificatWeekdayDefault>
|
||||
>;
|
||||
|
||||
function toResolvedText(proper: ProperText): ResolvedText {
|
||||
return { text: proper.text, status: proper.status, citation: proper.citation };
|
||||
@@ -163,6 +176,25 @@ export function getMinorHourOverrideId(day: LiturgicalDay): string | undefined {
|
||||
*/
|
||||
const PASCHALTIDE_SEASONS = new Set(['eastertide', 'ascensiontide', 'pentecost']);
|
||||
|
||||
/**
|
||||
* Which seasonal office-content suffix (if any) a day's season maps to —
|
||||
* shared by Lauds' and Vespers' own resolveOffice, both of which fall
|
||||
* back to the plain weekday default when this is undefined (and both
|
||||
* check a per-feast/octave override first, ahead of this). Paschaltide
|
||||
* covers eastertide/ascensiontide/pentecost uniformly, matching the
|
||||
* source's own single "Pasch" block for both hours — no separate
|
||||
* Ascension/Pentecost-specific chapter/hymn exists there.
|
||||
*/
|
||||
export function seasonalOfficeSuffix(season: string): string | undefined {
|
||||
if (season === 'advent' || season === 'lent' || season === 'passiontide') {
|
||||
return season;
|
||||
}
|
||||
if (PASCHALTIDE_SEASONS.has(season)) {
|
||||
return 'paschaltide';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The reference engine's mechanical Paschaltide fallback for a Common
|
||||
* antiphon with no dedicated wholesale-different text of its own: a
|
||||
@@ -449,6 +481,48 @@ export function getBenedictusAntiphon(day: LiturgicalDay): ResolvedText {
|
||||
return { text: {}, status: { la: 'missing', en: 'missing' } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Vespers' Magnificat antiphon — mirrors getBenedictusAntiphon's lookup
|
||||
* order exactly (temporal id, then a saint's own `${propers}-magnificat-
|
||||
* antiphon`, then their Common category's `magnificat-antiphon-${common}`
|
||||
* — none of that content authored yet, a separate future pass the same
|
||||
* shape as the Benedictus one was), but with one real difference: a bare
|
||||
* ferial weekday (Mon-Sat) has actual content to fall back to —
|
||||
* data/hours/vespers-magnificat-antiphons.yml's classic "quote the
|
||||
* Magnificat's own text in sequence" set — which Benedictus has no
|
||||
* equivalent of. `withPaschaltideAlleluia` applied to that fallback too,
|
||||
* by analogy with the Common-category treatment above; not independently
|
||||
* confirmed for this specific case.
|
||||
*/
|
||||
export function getMagnificatAntiphon(day: LiturgicalDay): ResolvedText {
|
||||
const winner = resolveOfficeWinner(day);
|
||||
if (winner.kind === 'temporal') {
|
||||
const named = toResolvedText(getTemporalProper(`${winner.id}-magnificat-antiphon`));
|
||||
if (named.status.la !== 'missing' || named.status.en !== 'missing') {
|
||||
return named;
|
||||
}
|
||||
const weekdayDefault = vespersMagnificatAntiphons[day.weekday];
|
||||
if (weekdayDefault) {
|
||||
const resolved = weekdayDefault.status
|
||||
? { text: weekdayDefault.antiphon, status: weekdayDefault.status }
|
||||
: verifiedText(weekdayDefault.antiphon);
|
||||
return withPaschaltideAlleluia(resolved, day);
|
||||
}
|
||||
return named;
|
||||
}
|
||||
const saint = getSaintRecord(winner.id);
|
||||
if (saint?.propers) {
|
||||
const proper = resolveCommon(`${saint.propers}-magnificat-antiphon`);
|
||||
if (proper.status.la !== 'missing' || proper.status.en !== 'missing') {
|
||||
return proper;
|
||||
}
|
||||
}
|
||||
if (saint?.benedictusCommon) {
|
||||
return withPaschaltideAlleluia(resolveCommon(`magnificat-antiphon-${saint.benedictusCommon}`), day);
|
||||
}
|
||||
return { text: {}, status: { la: 'missing', en: 'missing' } };
|
||||
}
|
||||
|
||||
export function verifiedText(text: Partial<Record<string, string>>): ResolvedText {
|
||||
const status: Partial<Record<string, 'verified'>> = {};
|
||||
for (const lang of Object.keys(text)) {
|
||||
|
||||
Reference in New Issue
Block a user