Add suffrages to Vespers, shared with Lauds' existing mechanism
Vespers had no suffrages (Holy Cross, BVM, Joseph, Apostles, Peace) at all -- types.ts even documented it as Lauds-only. Live-verified against the reference engine (Monastic Tridentinum 1617): the "Suffragium" block after the day's collect(s) renders byte-identical after both Lauds and Vespers, so this was an honest content gap, not a deliberate omission. Extracted the suffrage ids/labels and full omission logic (Double-or- higher, Christ the King, Advent/Christmastide/Passiontide, active octaves, Cross/Marian-Saturday special cases) out of lauds.ts into a shared resolveSuffrages() in resolve-common.ts, and wired it into vespers.ts/vespers.yml the same way Lauds already uses it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
This commit is contained in:
@@ -18,6 +18,13 @@
|
|||||||
# identical at both hours by live query) versus the fuller Tridentine
|
# identical at both hours by live query) versus the fuller Tridentine
|
||||||
# 1906/1910 ferial Preces on a ferial or vigil day — see hours/types.ts's
|
# 1906/1910 ferial Preces on a ferial or vigil day — see hours/types.ts's
|
||||||
# 'vespers-preces' doc comment.
|
# 'vespers-preces' doc comment.
|
||||||
|
#
|
||||||
|
# `suffrages` (Holy Cross, BVM, Joseph, Ss. Peter & Paul, Peace) reuses
|
||||||
|
# Lauds' own mechanism and content ids wholesale — live-verified
|
||||||
|
# 2026-08-25 against Monastic Tridentinum 1617 that the "Suffragium"
|
||||||
|
# block after Vespers' collect is byte-identical to the one after Lauds',
|
||||||
|
# not a Lauds-specific thing. See hours/resolve-common.ts's
|
||||||
|
# resolveSuffrages for the full omission logic.
|
||||||
id: vespers
|
id: vespers
|
||||||
parts:
|
parts:
|
||||||
- kind: opening-versicle
|
- kind: opening-versicle
|
||||||
@@ -26,5 +33,7 @@ parts:
|
|||||||
- kind: magnificat
|
- kind: magnificat
|
||||||
- kind: vespers-preces
|
- kind: vespers-preces
|
||||||
- kind: day-collects
|
- kind: day-collects
|
||||||
|
- kind: suffrages
|
||||||
|
omitOnDouble: true
|
||||||
- kind: preces
|
- kind: preces
|
||||||
textRef: { source: common, id: lauds-conclusio }
|
textRef: { source: common, id: lauds-conclusio }
|
||||||
|
|||||||
+5
-74
@@ -1,14 +1,13 @@
|
|||||||
import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart, ResolvedText } from './types';
|
import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart, ResolvedText } from './types';
|
||||||
import type { LiturgicalDay, Weekday } from '../calendar/types';
|
import type { LiturgicalDay, Weekday } from '../calendar/types';
|
||||||
import { resolveDay, isSundayOrFeast, activeOctavesFor } from '../calendar';
|
import { resolveDay } from '../calendar';
|
||||||
import { getDayLabel } from '../calendar/day-label';
|
import { getDayLabel } from '../calendar/day-label';
|
||||||
import { getTemporalFeastRecord } from '../calendar/temporal-feasts';
|
|
||||||
import { getPsalmVerses } from '../psalter';
|
import { getPsalmVerses } from '../psalter';
|
||||||
import { getCanticle } from './lauds-canticles';
|
import { getCanticle } from './lauds-canticles';
|
||||||
import { getLaudsPsalmodyOverride, type LaudsPsalmodyOverride } from './lauds-psalmody-overrides';
|
import { getLaudsPsalmodyOverride, type LaudsPsalmodyOverride } from './lauds-psalmody-overrides';
|
||||||
import { getOpeningVersicleId } from './opening-versicle';
|
import { getOpeningVersicleId } from './opening-versicle';
|
||||||
import { getMarianAntiphonId, getMarianAntiphonLabel } from './marian-antiphon';
|
import { getMarianAntiphonId, getMarianAntiphonLabel } from './marian-antiphon';
|
||||||
import { isDoubleOrHigher, applyFlexaMark } from './antiphon';
|
import { applyFlexaMark } from './antiphon';
|
||||||
import {
|
import {
|
||||||
resolveCommon,
|
resolveCommon,
|
||||||
getDayCollects,
|
getDayCollects,
|
||||||
@@ -22,6 +21,7 @@ import {
|
|||||||
getOfficeOverrideId,
|
getOfficeOverrideId,
|
||||||
resolveResponsory,
|
resolveResponsory,
|
||||||
resolveOfficeBundle,
|
resolveOfficeBundle,
|
||||||
|
resolveSuffrages,
|
||||||
} from './resolve-common';
|
} from './resolve-common';
|
||||||
import laudsDefinitionData from '../data/hours/lauds.yml';
|
import laudsDefinitionData from '../data/hours/lauds.yml';
|
||||||
import laudsAntiphonsData from '../data/hours/lauds-antiphons.yml';
|
import laudsAntiphonsData from '../data/hours/lauds-antiphons.yml';
|
||||||
@@ -210,21 +210,6 @@ function resolveOffice(day: LiturgicalDay): ResolvedPart[] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const SUFFRAGES = [
|
|
||||||
'lauds-suffrage-cross',
|
|
||||||
'lauds-suffrage-bvm',
|
|
||||||
'lauds-suffrage-joseph',
|
|
||||||
'lauds-suffrage-apostles',
|
|
||||||
'lauds-suffrage-peace',
|
|
||||||
];
|
|
||||||
const SUFFRAGE_LABELS: Record<string, string> = {
|
|
||||||
'lauds-suffrage-cross': 'Of the Holy Cross',
|
|
||||||
'lauds-suffrage-bvm': 'Of the Blessed Virgin Mary',
|
|
||||||
'lauds-suffrage-joseph': 'Of St. Joseph',
|
|
||||||
'lauds-suffrage-apostles': 'Of the Holy Apostles Peter and Paul',
|
|
||||||
'lauds-suffrage-peace': 'For Peace',
|
|
||||||
};
|
|
||||||
|
|
||||||
function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
||||||
switch (part.kind) {
|
switch (part.kind) {
|
||||||
case 'opening-versicle':
|
case 'opening-versicle':
|
||||||
@@ -246,62 +231,8 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
|||||||
return [{ kind: 'preces', text: resolveCommon(part.textRef.id), label: part.label }];
|
return [{ kind: 'preces', text: resolveCommon(part.textRef.id), label: part.label }];
|
||||||
case 'day-collects':
|
case 'day-collects':
|
||||||
return getDayCollects(day);
|
return getDayCollects(day);
|
||||||
case 'suffrages': {
|
case 'suffrages':
|
||||||
// isDoubleOrHigher only ever looks at a sanctoral rank — Christ the
|
return resolveSuffrages(day, part.omitOnDouble);
|
||||||
// King is modeled as a named temporal winner (see
|
|
||||||
// ALWAYS_OVERRIDE_TEMPORAL_IDS above) with no FeastClass to check at
|
|
||||||
// all, so it needs its own explicit inclusion here. Confirmed live:
|
|
||||||
// "Suffragium{omittitur}" — the whole set drops, same as any other
|
|
||||||
// Double-or-higher day.
|
|
||||||
const isChristTheKing = day.winner.kind === 'temporal' && day.winner.id === 'christ-the-king';
|
|
||||||
// Confirmed live against Monastic Tridentinum 1617 directly (this
|
|
||||||
// isn't a Tridentine-1906-only quirk -- the same exclusions hold
|
|
||||||
// under the app's own primary rubric track): suffrages are omitted
|
|
||||||
// *entirely* during Advent, Christmastide, and Passiontide (not all
|
|
||||||
// of Lent -- ordinary Lent ferias still get them, checked directly
|
|
||||||
// against 2026-02-20; only the last two weeks), and on any day
|
|
||||||
// within an active octave, however low that octave's own rank is
|
|
||||||
// -- checked directly against day 3 of St. Lawrence's own
|
|
||||||
// (Semiduplex) octave, which still omits them despite being well
|
|
||||||
// below the Duplex+ rank threshold below.
|
|
||||||
const isSeasonallyExcluded = day.season === 'advent' || day.season === 'christmastide' || day.season === 'passiontide';
|
|
||||||
const isWithinAnOctave = activeOctavesFor(day.date).length > 0;
|
|
||||||
if (part.omitOnDouble && (isDoubleOrHigher(day.winner) || isChristTheKing || isSeasonallyExcluded || isWithinAnOctave)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
// "Of the Holy Cross" is the real *ferial*-office suffrage, sourced
|
|
||||||
// from Tridentine 1906/1910 (a different track than the other four,
|
|
||||||
// which come from Monastic 1617 -- that track has no Cross suffrage
|
|
||||||
// at Lauds at all) -- live-verified against a plain ferial win, a
|
|
||||||
// plain Sunday win, a low-rank saint's own win (even bare Simplex),
|
|
||||||
// and Marian Saturday: Cross shows only when the bare temporal
|
|
||||||
// feria itself is what's being prayed -- the exact *opposite* of
|
|
||||||
// "Sunday or a feast of the Lord." In this app's own vocabulary,
|
|
||||||
// that's `!isSundayOrFeast(day)` (the same ferial/festive split
|
|
||||||
// Prime's capitulum already keys off) with one more exclusion for a
|
|
||||||
// named temporal identity like Marian Saturday, which isn't Sunday
|
|
||||||
// or a sanctoral winner either but still isn't a bare ferial office.
|
|
||||||
const showCross = !isSundayOrFeast(day) && !(day.winner.kind === 'temporal' && getTemporalFeastRecord(day.winner.id));
|
|
||||||
// "Of the Blessed Virgin Mary" is omitted specifically when the
|
|
||||||
// day's own office is already Marian (would be redundant to
|
|
||||||
// suffrage Mary again on her own day) -- confirmed live on Marian
|
|
||||||
// Saturday (Simplex-strength, so it reaches this far rather than
|
|
||||||
// being excluded outright above); the only case this app currently
|
|
||||||
// has of a day using the Marian common below the suffrage-omitting
|
|
||||||
// Duplex+ threshold (every actual Marian *feast* modeled so far is
|
|
||||||
// Duplex-majus+ and already returns above).
|
|
||||||
const isMarianSaturday = day.winner.kind === 'temporal' && day.winner.id === 'marian-saturday';
|
|
||||||
const ids = SUFFRAGES.filter((id) => {
|
|
||||||
if (id === 'lauds-suffrage-cross') return showCross;
|
|
||||||
if (id === 'lauds-suffrage-bvm') return !isMarianSaturday;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
return ids.map((id) => ({
|
|
||||||
kind: 'preces' as const,
|
|
||||||
text: resolveCommon(id),
|
|
||||||
label: SUFFRAGE_LABELS[id],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
case 'versicle':
|
case 'versicle':
|
||||||
case 'chapter':
|
case 'chapter':
|
||||||
case 'responsory':
|
case 'responsory':
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Commemoration, DayWinner, LiturgicalDay, Weekday } from '../calend
|
|||||||
import type { ProperText } from '../propers';
|
import type { ProperText } from '../propers';
|
||||||
import { getCommonProper, getTemporalProper } from '../propers';
|
import { getCommonProper, getTemporalProper } from '../propers';
|
||||||
import { getSaintRecord } from '../calendar/feasts';
|
import { getSaintRecord } from '../calendar/feasts';
|
||||||
import { resolveActiveOctave, activeOctavesFor, octaveGoverningPrivilegedDay, isAtLeast } from '../calendar';
|
import { resolveActiveOctave, activeOctavesFor, octaveGoverningPrivilegedDay, isAtLeast, isSundayOrFeast } from '../calendar';
|
||||||
import { getTemporalFeastRecord } from '../calendar/temporal-feasts';
|
import { getTemporalFeastRecord } from '../calendar/temporal-feasts';
|
||||||
import { isInTriduum } from '../calendar/temporal';
|
import { isInTriduum } from '../calendar/temporal';
|
||||||
import { splitAntiphon, isDoubleOrHigher } from './antiphon';
|
import { splitAntiphon, isDoubleOrHigher } from './antiphon';
|
||||||
@@ -618,6 +618,84 @@ export function getDayCollects(day: LiturgicalDay): ResolvedPart[] {
|
|||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SUFFRAGE_IDS = ['lauds-suffrage-cross', 'lauds-suffrage-bvm', 'lauds-suffrage-joseph', 'lauds-suffrage-apostles', 'lauds-suffrage-peace'];
|
||||||
|
const SUFFRAGE_LABELS: Record<string, string> = {
|
||||||
|
'lauds-suffrage-cross': 'Of the Holy Cross',
|
||||||
|
'lauds-suffrage-bvm': 'Of the Blessed Virgin Mary',
|
||||||
|
'lauds-suffrage-joseph': 'Of St. Joseph',
|
||||||
|
'lauds-suffrage-apostles': 'Of the Holy Apostles Peter and Paul',
|
||||||
|
'lauds-suffrage-peace': 'For Peace',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The four fixed Tridentine suffrages (Holy Cross, BVM, Joseph, Ss. Peter
|
||||||
|
* & Paul, Peace), said after the day's collect(s) — shared by Lauds and
|
||||||
|
* Vespers (live-verified 2026-08-25: Monastic Tridentinum 1617 renders
|
||||||
|
* the identical "Suffragium" block, same four antiphons/prayers, after
|
||||||
|
* *both* hours, not just Lauds — vu previously only had this mechanism
|
||||||
|
* wired up at Lauds, an honest content gap rather than a deliberate
|
||||||
|
* Lauds-only design). `omitOnDouble` gates the whole set at once (real
|
||||||
|
* practice: suffrages drop on a Double-or-higher feast) — see below for
|
||||||
|
* the caveat on what real practice also suppresses them for that isn't
|
||||||
|
* modeled.
|
||||||
|
*/
|
||||||
|
export function resolveSuffrages(day: LiturgicalDay, omitOnDouble: boolean | undefined): ResolvedPart[] {
|
||||||
|
// isDoubleOrHigher only ever looks at a sanctoral rank — Christ the
|
||||||
|
// King is modeled as a named temporal winner (see
|
||||||
|
// ALWAYS_OVERRIDE_TEMPORAL_IDS above) with no FeastClass to check at
|
||||||
|
// all, so it needs its own explicit inclusion here. Confirmed live:
|
||||||
|
// "Suffragium{omittitur}" — the whole set drops, same as any other
|
||||||
|
// Double-or-higher day.
|
||||||
|
const isChristTheKing = day.winner.kind === 'temporal' && day.winner.id === 'christ-the-king';
|
||||||
|
// Confirmed live against Monastic Tridentinum 1617 directly (this
|
||||||
|
// isn't a Tridentine-1906-only quirk -- the same exclusions hold
|
||||||
|
// under the app's own primary rubric track): suffrages are omitted
|
||||||
|
// *entirely* during Advent, Christmastide, and Passiontide (not all
|
||||||
|
// of Lent -- ordinary Lent ferias still get them, checked directly
|
||||||
|
// against 2026-02-20; only the last two weeks), and on any day
|
||||||
|
// within an active octave, however low that octave's own rank is
|
||||||
|
// -- checked directly against day 3 of St. Lawrence's own
|
||||||
|
// (Semiduplex) octave, which still omits them despite being well
|
||||||
|
// below the Duplex+ rank threshold below.
|
||||||
|
const isSeasonallyExcluded = day.season === 'advent' || day.season === 'christmastide' || day.season === 'passiontide';
|
||||||
|
const isWithinAnOctave = activeOctavesFor(day.date).length > 0;
|
||||||
|
if (omitOnDouble && (isDoubleOrHigher(day.winner) || isChristTheKing || isSeasonallyExcluded || isWithinAnOctave)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// "Of the Holy Cross" is the real *ferial*-office suffrage, sourced
|
||||||
|
// from Tridentine 1906/1910 (a different track than the other four,
|
||||||
|
// which come from Monastic 1617 -- that track has no Cross suffrage
|
||||||
|
// at Lauds at all) -- live-verified against a plain ferial win, a
|
||||||
|
// plain Sunday win, a low-rank saint's own win (even bare Simplex),
|
||||||
|
// and Marian Saturday: Cross shows only when the bare temporal
|
||||||
|
// feria itself is what's being prayed -- the exact *opposite* of
|
||||||
|
// "Sunday or a feast of the Lord." In this app's own vocabulary,
|
||||||
|
// that's `!isSundayOrFeast(day)` (the same ferial/festive split
|
||||||
|
// Prime's capitulum already keys off) with one more exclusion for a
|
||||||
|
// named temporal identity like Marian Saturday, which isn't Sunday
|
||||||
|
// or a sanctoral winner either but still isn't a bare ferial office.
|
||||||
|
const showCross = !isSundayOrFeast(day) && !(day.winner.kind === 'temporal' && getTemporalFeastRecord(day.winner.id));
|
||||||
|
// "Of the Blessed Virgin Mary" is omitted specifically when the
|
||||||
|
// day's own office is already Marian (would be redundant to
|
||||||
|
// suffrage Mary again on her own day) -- confirmed live on Marian
|
||||||
|
// Saturday (Simplex-strength, so it reaches this far rather than
|
||||||
|
// being excluded outright above); the only case this app currently
|
||||||
|
// has of a day using the Marian common below the suffrage-omitting
|
||||||
|
// Duplex+ threshold (every actual Marian *feast* modeled so far is
|
||||||
|
// Duplex-majus+ and already returns above).
|
||||||
|
const isMarianSaturday = day.winner.kind === 'temporal' && day.winner.id === 'marian-saturday';
|
||||||
|
const ids = SUFFRAGE_IDS.filter((id) => {
|
||||||
|
if (id === 'lauds-suffrage-cross') return showCross;
|
||||||
|
if (id === 'lauds-suffrage-bvm') return !isMarianSaturday;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return ids.map((id) => ({
|
||||||
|
kind: 'preces' as const,
|
||||||
|
text: resolveCommon(id),
|
||||||
|
label: SUFFRAGE_LABELS[id],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lauds' Benedictus antiphon, resolved the same way as getDayCollect: a
|
* Lauds' Benedictus antiphon, resolved the same way as getDayCollect: a
|
||||||
* per-saint `${propers}-antiphon` (already authored during the sanctoral
|
* per-saint `${propers}-antiphon` (already authored during the sanctoral
|
||||||
|
|||||||
+9
-6
@@ -140,12 +140,15 @@ export type HourPart =
|
|||||||
// full-after-Creed... no Creed at Lauds, just before+after framing shape
|
// full-after-Creed... no Creed at Lauds, just before+after framing shape
|
||||||
// as nunc-dimittis, gated the same way by isDoubleOrHigher.
|
// as nunc-dimittis, gated the same way by isDoubleOrHigher.
|
||||||
| { kind: 'benedictus' }
|
| { kind: 'benedictus' }
|
||||||
// Lauds only. The four fixed Tridentine suffrages (Holy Cross, BVM,
|
// Lauds and Vespers. The four fixed Tridentine suffrages (Holy Cross,
|
||||||
// Ss. Peter & Paul, Peace) said after the day's collect(s) — see
|
// BVM, Ss. Peter & Paul, Peace) said after the day's collect(s) — see
|
||||||
// data/propers/common/lauds-suffrage-*.yml. `omitOnDouble` gates the
|
// data/propers/common/lauds-suffrage-*.yml (reused byte-identical at
|
||||||
// whole set at once (real practice: suffrages drop on a Double-or-higher
|
// Vespers, same fixed formula after either hour, live-verified
|
||||||
// feast) — an honest simplification, see hours/lauds.ts for the caveat
|
// 2026-08-25 — see hours/resolve-common.ts's resolveSuffrages).
|
||||||
// on what real practice also suppresses them for that isn't modeled.
|
// `omitOnDouble` gates the whole set at once (real practice: suffrages
|
||||||
|
// drop on a Double-or-higher feast) — an honest simplification, see
|
||||||
|
// resolveSuffrages for the caveat on what real practice also
|
||||||
|
// suppresses them for that isn't modeled.
|
||||||
| { kind: 'suffrages'; omitOnDouble?: true }
|
| { kind: 'suffrages'; omitOnDouble?: true }
|
||||||
// Lauds only. The Tridentine 1906/1910 "Preces feriales" litany (Kyrie,
|
// Lauds only. The Tridentine 1906/1910 "Preces feriales" litany (Kyrie,
|
||||||
// Pater noster, a long chain of versicles, Psalm 129, closing versicles)
|
// Pater noster, a long chain of versicles, Psalm 129, closing versicles)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
isFerialOrVigil,
|
isFerialOrVigil,
|
||||||
resolveResponsory,
|
resolveResponsory,
|
||||||
resolveOfficeBundle,
|
resolveOfficeBundle,
|
||||||
|
resolveSuffrages,
|
||||||
} from './resolve-common';
|
} from './resolve-common';
|
||||||
import vespersDefinitionData from '../data/hours/vespers.yml';
|
import vespersDefinitionData from '../data/hours/vespers.yml';
|
||||||
import vespersAntiphonsData from '../data/hours/vespers-antiphons.yml';
|
import vespersAntiphonsData from '../data/hours/vespers-antiphons.yml';
|
||||||
@@ -176,6 +177,8 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
|||||||
}
|
}
|
||||||
case 'day-collects':
|
case 'day-collects':
|
||||||
return getDayCollects(day);
|
return getDayCollects(day);
|
||||||
|
case 'suffrages':
|
||||||
|
return resolveSuffrages(day, part.omitOnDouble);
|
||||||
case 'preces':
|
case 'preces':
|
||||||
return [{ kind: 'preces', text: resolveCommon(part.textRef.id), label: part.label }];
|
return [{ kind: 'preces', text: resolveCommon(part.textRef.id), label: part.label }];
|
||||||
case 'versicle':
|
case 'versicle':
|
||||||
@@ -199,7 +202,6 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
|||||||
case 'lauds-psalmody':
|
case 'lauds-psalmody':
|
||||||
case 'lauds-office':
|
case 'lauds-office':
|
||||||
case 'benedictus':
|
case 'benedictus':
|
||||||
case 'suffrages':
|
|
||||||
case 'lauds-preces':
|
case 'lauds-preces':
|
||||||
case 'marian-antiphon':
|
case 'marian-antiphon':
|
||||||
throw new Error(`Vespers' ordo doesn't support a '${part.kind}' part`);
|
throw new Error(`Vespers' ordo doesn't support a '${part.kind}' part`);
|
||||||
|
|||||||
Reference in New Issue
Block a user