Add per-feast Vespers office overrides for all 30 duplex-majus+ ids
Same eligibility rule as Lauds' psalmody override (a duplex-majus+ saint, or a named temporal feast), factored out to resolve-common.ts's getOfficeOverrideId since Vespers has no psalmody-override table of its own to piggyback eligibility on. Authored via one live command=prayVespera query per id, reusing the same clean per-saint dates already established for each one's Lauds office-bundle override. The capitulum turned out to be byte-identical between Lauds and Vespers for every one of the 30 ids checked, a real Monastic-rite fact rather than a coincidence — so no vespers-capitulum-<id>.yml files exist at all; the chapter falls through to the already-authored lauds-capitulum-<id> directly instead of duplicating identical content. St. Scholastica's Vespers responsory, hymn, and versicle needed a hand translation (same gap she has at Lauds/Terce/Sext/None — the reference engine has no English for any of her proper content). Two more one-line English gaps in the shared Immaculate Conception/Nativity of the BVM responsory were filled with the standard Douay-Rheims/Ave Maria wording. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+13
-18
@@ -1,6 +1,6 @@
|
||||
import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart, ResolvedText } from './types';
|
||||
import type { LiturgicalDay, Weekday } from '../calendar/types';
|
||||
import { resolveDay, isAtLeast, isSundayOrFeast, activeOctavesFor } from '../calendar';
|
||||
import { resolveDay, isSundayOrFeast, activeOctavesFor } from '../calendar';
|
||||
import { getDayLabel } from '../calendar/day-label';
|
||||
import { getTemporalFeastRecord } from '../calendar/temporal-feasts';
|
||||
import { getPsalmVerses } from '../psalter';
|
||||
@@ -15,10 +15,10 @@ import {
|
||||
getBenedictusAntiphon,
|
||||
splitNamedAntiphon,
|
||||
resolveOfficeWinner,
|
||||
ALWAYS_OVERRIDE_TEMPORAL_IDS,
|
||||
verifiedText,
|
||||
seasonalOfficeSuffix,
|
||||
isFerialOrVigil,
|
||||
getOfficeOverrideId,
|
||||
} from './resolve-common';
|
||||
import laudsDefinitionData from '../data/hours/lauds.yml';
|
||||
import laudsAntiphonsData from '../data/hours/lauds-antiphons.yml';
|
||||
@@ -47,28 +47,23 @@ const laudsAntiphons = laudsAntiphonsData as Record<Weekday, PsalmodyBlock>;
|
||||
|
||||
/**
|
||||
* A duplex-majus+ saint's own proper psalmody (per-feast, not per-Common —
|
||||
* explicit choice), or one of the named temporal feasts in
|
||||
* ALWAYS_OVERRIDE_TEMPORAL_IDS. Falls back to `undefined` — the plain
|
||||
* weekday default — for a duplex-majus+ saint with no override authored
|
||||
* explicit choice), or one of the named temporal feasts eligible via
|
||||
* resolve-common.ts's getOfficeOverrideId. Falls back to `undefined` — the
|
||||
* plain weekday default — for an eligible feast with no override authored
|
||||
* yet, same honest incremental-content convention as everywhere else in
|
||||
* this codebase; it's not gated behind whether content exists, just
|
||||
* behind whether it's *eligible* to override at all.
|
||||
*
|
||||
* Checks resolveOfficeWinner(day), not the raw `day.winner` — on an
|
||||
* octave day this is the octave's own feast (e.g. St. Lawrence, days 2-8
|
||||
* of his own octave), which is exactly what should supply the psalmody
|
||||
* override there too, live-verified alongside the collect/Benedictus-
|
||||
* antiphon fix (see resolve-common.ts's resolveOfficeWinner doc comment).
|
||||
* getOfficeOverrideId itself checks resolveOfficeWinner(day), not the raw
|
||||
* `day.winner` — on an octave day this is the octave's own feast (e.g. St.
|
||||
* Lawrence, days 2-8 of his own octave), which is exactly what should
|
||||
* supply the psalmody override there too, live-verified alongside the
|
||||
* collect/Benedictus-antiphon fix (see resolve-common.ts's
|
||||
* resolveOfficeWinner doc comment).
|
||||
*/
|
||||
function getPsalmodyOverrideFor(day: LiturgicalDay): LaudsPsalmodyOverride | undefined {
|
||||
const winner = resolveOfficeWinner(day);
|
||||
if (winner.kind === 'temporal' && ALWAYS_OVERRIDE_TEMPORAL_IDS.has(winner.id)) {
|
||||
return getLaudsPsalmodyOverride(winner.id);
|
||||
}
|
||||
if (winner.kind === 'sanctoral' && isAtLeast(winner.rank, 'duplex-majus')) {
|
||||
return getLaudsPsalmodyOverride(winner.id);
|
||||
}
|
||||
return undefined;
|
||||
const id = getOfficeOverrideId(day);
|
||||
return id ? getLaudsPsalmodyOverride(id) : undefined;
|
||||
}
|
||||
|
||||
// Mon-Fri share one capitulum verbatim (confirmed against the live engine
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Commemoration, DayWinner, LiturgicalDay, Weekday } from '../calend
|
||||
import type { ProperText } from '../propers';
|
||||
import { getCommonProper, getTemporalProper } from '../propers';
|
||||
import { getSaintRecord } from '../calendar/feasts';
|
||||
import { resolveActiveOctave, activeOctavesFor } from '../calendar';
|
||||
import { resolveActiveOctave, activeOctavesFor, isAtLeast } from '../calendar';
|
||||
import { getTemporalFeastRecord } from '../calendar/temporal-feasts';
|
||||
import { splitAntiphon } from './antiphon';
|
||||
import vespersMagnificatAntiphonsData from '../data/hours/vespers-magnificat-antiphons.yml';
|
||||
@@ -131,6 +131,30 @@ export function isFerialOrVigil(day: LiturgicalDay): boolean {
|
||||
return day.weekday !== 'sunday' && !isWithinAnOctave && (isVigil || isBareFeria);
|
||||
}
|
||||
|
||||
/**
|
||||
* A duplex-majus+ saint's own id, or one of the named temporal feasts in
|
||||
* ALWAYS_OVERRIDE_TEMPORAL_IDS — the same rank-eligibility test
|
||||
* hours/lauds.ts's getPsalmodyOverrideFor uses (and originally
|
||||
* duplicated), factored out here so hours/vespers.ts's own per-feast
|
||||
* office override (chapter/responsory/hymn/versicle — Vespers has no
|
||||
* psalmody-override table of its own to piggyback eligibility on, unlike
|
||||
* Lauds) can share the identical eligibility rule. Callers still decide
|
||||
* for themselves whether anything's actually been authored for the id
|
||||
* returned — same honest incremental-content convention as everywhere
|
||||
* else; this only answers "is this day's winner eligible to override at
|
||||
* all," not "does an override exist."
|
||||
*/
|
||||
export function getOfficeOverrideId(day: LiturgicalDay): string | undefined {
|
||||
const winner = resolveOfficeWinner(day);
|
||||
if (winner.kind === 'temporal' && ALWAYS_OVERRIDE_TEMPORAL_IDS.has(winner.id)) {
|
||||
return winner.id;
|
||||
}
|
||||
if (winner.kind === 'sanctoral' && isAtLeast(winner.rank, 'duplex-majus')) {
|
||||
return winner.id;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The id to look up for today's office winner's (resolveOfficeWinner)
|
||||
* minor-hour content (antiphon + chapter) — `${hourId}-antiphon-${id}`/
|
||||
|
||||
+46
-6
@@ -1,4 +1,4 @@
|
||||
import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart } from './types';
|
||||
import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart, ResolvedText } from './types';
|
||||
import type { LiturgicalDay, Weekday } from '../calendar/types';
|
||||
import { resolveEveningDay } from '../calendar/vespers';
|
||||
import { getDayLabel } from '../calendar/day-label';
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
verifiedText,
|
||||
seasonalOfficeSuffix,
|
||||
isFerialOrVigil,
|
||||
getOfficeOverrideId,
|
||||
} from './resolve-common';
|
||||
import vespersDefinitionData from '../data/hours/vespers.yml';
|
||||
import vespersAntiphonsData from '../data/hours/vespers-antiphons.yml';
|
||||
@@ -81,13 +82,52 @@ function vespersVersicleId(weekday: Weekday): string {
|
||||
return weekday === 'saturday' ? 'vespers-versicle-saturday' : 'vespers-versicle-sunday-ferial';
|
||||
}
|
||||
|
||||
/** A per-feast override's chapter: `vespers-capitulum-${id}` when
|
||||
* authored, else the already-authored `lauds-capitulum-${id}` directly —
|
||||
* live-verified across all 30 duplex-majus+ saints/named-temporal ids
|
||||
* with a Vespers office override (2026-08 sweep, `command=prayVespera`,
|
||||
* each saint's own already-established clean date): the capitulum is
|
||||
* byte-identical between Lauds and Vespers for every one of them, a real
|
||||
* Monastic-rite fact (the reading doesn't change between the two hours,
|
||||
* only the responsory/hymn/versicle do), not a coincidence specific to
|
||||
* the ones checked. So no `vespers-capitulum-<id>.yml` files were
|
||||
* authored at all — this falls straight through to the Lauds one instead
|
||||
* of duplicating identical content into a second file per id. */
|
||||
function vespersCapitulumForOverride(id: string): ResolvedText {
|
||||
const proper = resolveCommon(`vespers-capitulum-${id}`);
|
||||
if (proper.status.la !== 'missing' || proper.status.en !== 'missing') {
|
||||
return proper;
|
||||
}
|
||||
return resolveCommon(`lauds-capitulum-${id}`);
|
||||
}
|
||||
|
||||
/** The chapter/responsory/hymn/versicle bundle following the psalmody —
|
||||
* see hours/types.ts's 'vespers-office' doc comment. No per-feast
|
||||
* override mechanism yet, unlike Lauds' resolveOffice. Falls to a
|
||||
* *seasonal* default first (Advent/Lent/Passiontide/Paschaltide — see
|
||||
* resolve-common.ts's seasonalOfficeSuffix), then the plain weekday
|
||||
* default. */
|
||||
* see hours/types.ts's 'vespers-office' doc comment. A duplex-majus+
|
||||
* saint's own proper bundle (chapter via vespersCapitulumForOverride
|
||||
* above, responsory/hymn/versicle via `vespers-{part}-${id}`), or one of
|
||||
* the named temporal feasts eligible via resolve-common.ts's
|
||||
* getOfficeOverrideId, wins first when authored — same eligible-but-not-
|
||||
* gated-on-content pattern as Lauds' resolveOffice, just without a
|
||||
* psalmody-override table of its own to piggyback eligibility on
|
||||
* (Vespers has no per-feast psalmody override mechanism — see TODO.md).
|
||||
* Falls to a *seasonal* default next (Advent/Lent/Passiontide/
|
||||
* Paschaltide — see resolve-common.ts's seasonalOfficeSuffix), and only
|
||||
* then the plain weekday default — a feast's own override always wins
|
||||
* over the season it happens to fall in, same as everywhere else in this
|
||||
* codebase. */
|
||||
function resolveOffice(day: LiturgicalDay): ResolvedPart[] {
|
||||
const overrideId = getOfficeOverrideId(day);
|
||||
if (overrideId) {
|
||||
const chapter = vespersCapitulumForOverride(overrideId);
|
||||
if (chapter.status.la !== 'missing' || chapter.status.en !== 'missing') {
|
||||
return [
|
||||
{ kind: 'chapter', text: chapter },
|
||||
{ kind: 'responsory', text: resolveCommon(`vespers-responsory-${overrideId}`) },
|
||||
{ kind: 'hymn', text: resolveCommon(`vespers-hymn-${overrideId}`) },
|
||||
{ kind: 'versicle', text: resolveCommon(`vespers-versicle-${overrideId}`) },
|
||||
];
|
||||
}
|
||||
}
|
||||
const seasonSuffix = seasonalOfficeSuffix(day.season);
|
||||
const key = seasonSuffix ?? day.weekday;
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user