Add Vespers' ferial Preces, sourced from Tridentine 1906/1910

Monastic 1617 has no ferial Preces at Vespers, same gap Lauds already
had — the fuller "Preces feriales Vespera" is byte-for-byte the Lauds
ferial Preces with Psalm 129 swapped for Psalm 50 in the source, live-
verified against the same clean Advent feria already used for Lauds.
The short Sunday/feast form turns out to be identical at both hours too,
so it's reused directly rather than duplicated.

Factors the shared ferial-or-vigil predicate out of hours/lauds.ts into
resolve-common.ts's isFerialOrVigil so both hours' -preces cases resolve
it the same way instead of keeping two copies in sync by hand.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 21:30:01 -04:00
parent ce954fea9a
commit 4d88568c42
12 changed files with 242 additions and 20 deletions
+7
View File
@@ -12,12 +12,19 @@
# version of this exact closing turned up in the source, and it's the
# same fixed formula in real practice after either hour; flagged here in
# case that assumption turns out wrong.
#
# `vespers-preces` mirrors Lauds' own `lauds-preces` exactly: the short
# Sunday/feast litany (reusing `lauds-short-litany` — confirmed byte-
# 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
# 'vespers-preces' doc comment.
id: vespers
parts:
- kind: opening-versicle
- kind: vespers-psalmody
- kind: vespers-office
- kind: magnificat
- kind: vespers-preces
- kind: day-collects
- kind: preces
textRef: { source: common, id: lauds-conclusio }
@@ -0,0 +1,150 @@
# Verified against Divinum Officium ("Tridentine - 1906", i.e. `version=
# Tridentine 1910`), Vespers live query, the same clean Advent feria
# 2026-12-17 already used for lauds-preces-feriales.yml. Source
# (Psalterium/Special/Preces.txt) defines this section as
# `[Preces feriales Vespera] @:Preces feriales Laudes:s/129/50/` -- i.e.
# byte-for-byte identical to the Lauds ferial Preces except Psalm 129 (De
# profundis) is replaced by Psalm 50 (Miserere) -- confirmed directly
# against the live render, same omission pattern as Lauds: the Pope/Bishop
# versicle pair and the benefactors' versicle are both omitted live despite
# being present in the underlying template, while the King/nation pair
# *is* rendered -- transcribed as rendered, not reconstructed from the
# template's own conditional markup. Monastic 1617 has no ferial Preces at
# Vespers at all, same gap as Lauds -- sourced from this different track
# per the same direct instruction. Replaces a plain `preces` part with
# `textRef: lauds-conclusio` on a ferial or vigil day -- see hours/
# vespers.ts's 'vespers-preces' case for the exact predicate, mirroring
# hours/lauds.ts's 'lauds-preces' case exactly.
id: vespers-preces-feriales
text:
la: |
Kýrie, eléison. Christe, eléison. Kýrie, eléison.
Pater noster, qui es in cælis, sanctificétur nomen tuum: advéniat regnum tuum: fiat volúntas tua, sicut in cælo et in terra. Panem nostrum quotidiánum da nobis hódie: et dimítte nobis débita nostra, sicut et nos dimíttimus debitóribus nostris:
V. Et ne nos indúcas in tentatiónem:
R. Sed líbera nos a malo.
V. Ego dixi: Dómine, miserére mei.
R. Sana ánimam meam quia peccávi tibi.
V. Convértere, Dómine, úsquequo?
R. Et deprecábilis esto super servos tuos.
V. Fiat misericórdia tua, Dómine, super nos.
R. Quemádmodum sperávimus in te.
V. Sacerdótes tui induántur justítiam.
R. Et sancti tui exsúltent.
V. Dómine, salvum fac regem.
R. Et exáudi nos in die, qua invocavérimus te.
V. Salvum fac pópulum tuum, Dómine, et bénedic hereditáti tuæ.
R. Et rege eos, et extólle illos usque in ætérnum.
V. Meménto Congregatiónis tuæ.
R. Quam possedísti ab inítio.
V. Fiat pax in virtúte tua.
R. Et abundántia in túrribus tuis.
V. Orémus pro fidélibus defúnctis.
R. Réquiem ætérnam dona eis, Dómine, et lux perpétua lúceat eis.
V. Requiéscant in pace.
R. Amen.
V. Pro frátribus nostris abséntibus.
R. Salvos fac servos tuos, Deus meus, sperántes in te.
V. Pro afflíctis et captívis.
R. Líbera eos, Deus Israël, ex ómnibus tribulatiónibus suis.
V. Mitte eis, Dómine, auxílium de sancto.
R. Et de Sion tuére eos.
V. Dómine, exáudi oratiónem meam.
R. Et clamor meus ad te véniat.
Psalmus 50.
Miserére mei, Deus, secúndum magnam misericórdiam tuam.
Et secúndum multitúdinem miseratiónum tuárum, dele iniquitátem meam.
Ámplius lava me ab iniquitáte mea: et a peccáto meo munda me.
Quóniam iniquitátem meam ego cognósco: et peccátum meum contra me est semper.
Tibi soli peccávi, et malum coram te feci: ut justificéris in sermónibus tuis, et vincas cum judicáris.
Ecce enim, in iniquitátibus concéptus sum: et in peccátis concépit me mater mea.
Ecce enim, veritátem dilexísti: incérta et occúlta sapiéntiæ tuæ manifestásti mihi.
Aspérges me hyssópo, et mundábor: lavábis me, et super nivem dealbábor.
Audítui meo dabis gáudium et lætítiam: et exsultábunt ossa humiliáta.
Avérte fáciem tuam a peccátis meis: et omnes iniquitátes meas dele.
Cor mundum crea in me, Deus: et spíritum rectum ínnova in viscéribus meis.
Ne proícias me a fácie tua: et spíritum sanctum tuum ne áuferas a me.
Redde mihi lætítiam salutáris tui: et spíritu principáli confírma me.
Docébo iníquos vias tuas: et ímpii ad te converténtur.
Líbera me de sanguínibus, Deus, Deus salútis meæ: et exsultábit lingua mea justítiam tuam.
Dómine, lábia mea apéries: et os meum annuntiábit laudem tuam.
Quóniam si voluísses sacrifícium, dedíssem útique: holocáustis non delectáberis.
Sacrifícium Deo spíritus contribulátus: cor contrítum, et humiliátum, Deus, non despícies.
Benígne fac, Dómine, in bona voluntáte tua Sion: ut ædificéntur muri Jerúsalem.
Tunc acceptábis sacrifícium justítiæ, oblatiónes, et holocáusta: tunc impónent super altáre tuum vítulos.
V. Glória Patri, et Fílio, et Spirítui Sancto.
R. Sicut erat in princípio, et nunc, et semper, et in sǽcula sæculórum. Amen.
V. Dómine, Deus virtútum, convérte nos.
R. Et osténde fáciem tuam, et salvi érimus.
V. Exsúrge, Christe, ádjuva nos.
R. Et líbera nos propter nomen tuum.
V. Dómine, exáudi oratiónem meam.
R. Et clamor meus ad te véniat.
en: |
Lord have mercy upon us, Christ have mercy upon us, Lord have mercy upon us.
Our Father, who art in heaven, Hallowed be thy name. Thy kingdom come. Thy will be done on earth as it is in heaven. Give us this day our daily bread. And forgive us our trespasses, as we forgive those who trespass against us.
V. And lead us not into temptation:
R. But deliver us from evil.
V. I said: Lord, be merciful unto me:
R. Heal my soul, for I have sinned against thee.
V. Turn thee again, O Lord; how long will it be?
R. And be gracious unto thy servants.
V. Let thy mercy, O Lord, be upon us.
R. As we have hoped in thee.
V. Let thy priests be clothed with justice:
R. And may thy saints rejoice.
V. O Lord, save our leaders.
R. And mercifully hear us when we call upon thee.
V. O Lord, save thy people, and bless thine inheritance:
R. Govern them and lift them up for ever.
V. Remember thy congregation,
R. Which thou hast possessed from the beginning.
V. Let peace be in thy strength:
R. And abundance in thy towers.
V. Let us pray for the faithful departed.
R. Eternal rest grant unto them, O Lord, and let perpetual light shine upon them.
V. May they rest in peace.
R. Amen.
V. Let us pray for our absent brothers.
R. Save thy servants, O God, who put their trust in thee.
V. Let us pray for the afflicted and imprisoned.
R. Deliver them, God of Israel, from all their tribulations.
V. O Lord, send them help from thy sanctuary.
R. And defend them out of Sion.
V. O Lord, hear my prayer.
R. And let my cry come unto thee.
Psalm 50.
Have mercy on me, O God, according to thy great mercy.
And according to the multitude of thy tender mercies blot out my iniquity.
Wash me yet more from my iniquity, and cleanse me from my sin.
For I know my iniquity, and my sin is always before me.
To thee only have I sinned, and have done evil before thee: that thou mayst be justified in thy words, and mayst overcome when thou art judged.
For behold I was conceived in iniquities; and in sins did my mother conceive me.
For behold thou hast loved truth: the uncertain and hidden things of thy wisdom thou hast made manifest to me.
Thou shalt sprinkle me with hyssop, and I shall be cleansed: thou shalt wash me, and I shall be made whiter than snow.
To my hearing thou shalt give joy and gladness: and the bones that have been humbled shall rejoice.
Turn away thy face from my sins, and blot out all my iniquities.
Create a clean heart in me, O God: and renew a right spirit within my bowels.
Cast me not away from thy face; and take not thy holy spirit from me.
Restore unto me the joy of thy salvation, and strengthen me with a perfect spirit.
I will teach the unjust thy ways: and the wicked shall be converted to thee.
Deliver me from blood, O God, thou God of my salvation: and my tongue shall extol thy justice.
O Lord, thou wilt open my lips: and my mouth shall declare thy praise.
For if thou hadst desired sacrifice, I would indeed have given it: with burnt offerings thou wilt not be delighted.
A sacrifice to God is an afflicted spirit: a contrite and humbled heart, O God, thou wilt not despise.
Deal favourably, O Lord, in thy good will with Sion; that the walls of Jerusalem may be built up.
Then shalt thou accept the sacrifice of justice, oblations and whole burnt offerings: then shall they lay calves upon thy altar.
V. Glory be to the Father, and to the Son, and to the Holy Ghost.
R. As it was in the beginning, is now, and ever shall be, world without end. Amen.
V. Turn us again, O Lord, God of Hosts.
R. Show us thy face, and we shall be whole.
V. Arise, O Christ, and help us.
R. And redeem us for thy name's sake.
V. O Lord, hear my prayer.
R. And let my cry come unto thee.
status:
la: verified
en: verified
+1
View File
@@ -94,6 +94,7 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
case 'vespers-psalmody':
case 'vespers-office':
case 'magnificat':
case 'vespers-preces':
throw new Error(`Compline's ordo doesn't support a '${part.kind}' part`);
}
}
+3 -19
View File
@@ -18,6 +18,7 @@ import {
ALWAYS_OVERRIDE_TEMPORAL_IDS,
verifiedText,
seasonalOfficeSuffix,
isFerialOrVigil,
} from './resolve-common';
import laudsDefinitionData from '../data/hours/lauds.yml';
import laudsAntiphonsData from '../data/hours/lauds-antiphons.yml';
@@ -317,25 +318,7 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
return [{ kind: 'preces', text: resolveCommon(id), label: getMarianAntiphonLabel(id) }];
}
case 'lauds-preces': {
// Deliberately not `isSundayOrFeast` -- that treats any sanctoral
// winner, Vigils included, as a "feast." Here a Vigil needs to land
// on the ferial side instead (per direct instruction), so it's
// checked for explicitly: a Vigil-ranked sanctoral winner, or a
// bare temporal winner that isn't Sunday and isn't a named
// temporal identity like Marian Saturday or Christ the King (which
// aren't Sunday or a sanctoral winner either, but still aren't a
// bare ferial office -- same reasoning as the Cross suffrage's own
// gate just above). Also excludes any day within an active octave
// -- live-verified (Tridentine 1910, day 3 of St. Lawrence's own
// octave): "Preces Feriales{omittitur}", same exclusion the
// suffrages already have, for the same reason -- an octave day
// isn't a bare ferial office even though nothing else is winning
// it outright.
const isVigil = day.winner.kind === 'sanctoral' && day.winner.rank === 'vigil';
const isBareFeria = day.winner.kind === 'temporal' && !getTemporalFeastRecord(day.winner.id);
const isWithinAnOctave = activeOctavesFor(day.date).length > 0;
const isFerialOrVigil = day.weekday !== 'sunday' && !isWithinAnOctave && (isVigil || isBareFeria);
const id = isFerialOrVigil ? 'lauds-preces-feriales' : 'lauds-short-litany';
const id = isFerialOrVigil(day) ? 'lauds-preces-feriales' : 'lauds-short-litany';
return [{ kind: 'preces', text: resolveCommon(id) }];
}
// Not used by Lauds.
@@ -354,6 +337,7 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
case 'vespers-psalmody':
case 'vespers-office':
case 'magnificat':
case 'vespers-preces':
throw new Error(`Lauds' ordo doesn't support a '${part.kind}' part`);
}
}
+1
View File
@@ -70,6 +70,7 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
case 'vespers-psalmody':
case 'vespers-office':
case 'magnificat':
case 'vespers-preces':
throw new Error(`None's ordo doesn't support a '${part.kind}' part`);
}
}
+1
View File
@@ -126,6 +126,7 @@ function resolvePart(part: HourPart, date: string, day: LiturgicalDay): Resolved
case 'vespers-psalmody':
case 'vespers-office':
case 'magnificat':
case 'vespers-preces':
throw new Error(`Prime's ordo doesn't support a '${part.kind}' part`);
}
}
+27 -1
View File
@@ -3,7 +3,8 @@ 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 } from '../calendar';
import { resolveActiveOctave, activeOctavesFor } from '../calendar';
import { getTemporalFeastRecord } from '../calendar/temporal-feasts';
import { splitAntiphon } from './antiphon';
import vespersMagnificatAntiphonsData from '../data/hours/vespers-magnificat-antiphons.yml';
@@ -105,6 +106,31 @@ export function isSundayOrFeastOffice(day: LiturgicalDay): boolean {
return day.weekday === 'sunday' || resolveOfficeWinner(day).kind === 'sanctoral';
}
/**
* A day/hour is "ferial or vigil" — the predicate both Lauds' and Vespers'
* ferial Preces (`lauds-preces`/`vespers-preces`) key off to pick the
* fuller Tridentine 1906/1910 litany over the short Sunday/feast one.
* Deliberately not `isSundayOrFeast`/`isSundayOrFeastOffice` — those treat
* any sanctoral winner, Vigils included, as a "feast." Here a Vigil needs
* to land on the ferial side instead (per direct instruction), so it's
* checked for explicitly: a Vigil-ranked sanctoral winner, or a bare
* temporal winner that isn't Sunday and isn't a named temporal identity
* like Marian Saturday or Christ the King (which aren't Sunday or a
* sanctoral winner either, but still aren't a bare ferial office — same
* reasoning as Lauds' own Cross-suffrage gate). Also excludes any day
* within an active octave — live-verified (Tridentine 1910, day 3 of St.
* Lawrence's own octave, both Lauds and Vespers): "Preces Feriales
* {omittitur}", same exclusion the suffrages already have, for the same
* reason — an octave day isn't a bare ferial office even though nothing
* else is winning it outright.
*/
export function isFerialOrVigil(day: LiturgicalDay): boolean {
const isVigil = day.winner.kind === 'sanctoral' && day.winner.rank === 'vigil';
const isBareFeria = day.winner.kind === 'temporal' && !getTemporalFeastRecord(day.winner.id);
const isWithinAnOctave = activeOctavesFor(day.date).length > 0;
return day.weekday !== 'sunday' && !isWithinAnOctave && (isVigil || isBareFeria);
}
/**
* The id to look up for today's office winner's (resolveOfficeWinner)
* minor-hour content (antiphon + chapter) — `${hourId}-antiphon-${id}`/
+1
View File
@@ -70,6 +70,7 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
case 'vespers-psalmody':
case 'vespers-office':
case 'magnificat':
case 'vespers-preces':
throw new Error(`Sext's ordo doesn't support a '${part.kind}' part`);
}
}
+1
View File
@@ -70,6 +70,7 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
case 'vespers-psalmody':
case 'vespers-office':
case 'magnificat':
case 'vespers-preces':
throw new Error(`Terce's ordo doesn't support a '${part.kind}' part`);
}
}
+13
View File
@@ -155,6 +155,19 @@ export type HourPart =
// deliberately not the shared `isSundayOrFeast` (a Vigil counts as a
// "feast" there; here it needs to count as ferial-like instead).
| { kind: 'lauds-preces' }
// Vespers only. Mirrors 'lauds-preces' exactly — the Tridentine
// 1906/1910 "Preces feriales Vespera" (source: `@:Preces feriales
// Laudes:s/129/50/` — byte-for-byte the Lauds ferial Preces with Psalm
// 129 swapped for Psalm 50, live-verified against a clean Advent feria
// 2026-12-17) on a ferial or vigil day, replacing the short Sunday/feast
// litany otherwise said before the day's collect(s) — see hours/
// resolve-common.ts's isFerialOrVigil (shared with 'lauds-preces') and
// hours/vespers.ts's 'vespers-preces' case. Unlike Lauds, the short form
// here reuses `lauds-short-litany` directly rather than a Vespers-named
// duplicate — confirmed byte-identical by live query at both hours,
// same reuse discipline as data/hours/vespers.yml's `lauds-conclusio`
// closing.
| { kind: 'vespers-preces' }
// Lauds/Vespers. The day's own collect *and* one more per commemoration
// (calendar/types.ts's LiturgicalDay.commemorations) — see hours/
// resolve-common.ts's getDayCollects. Distinct from the singular
+5
View File
@@ -13,6 +13,7 @@ import {
resolveOfficeWinner,
verifiedText,
seasonalOfficeSuffix,
isFerialOrVigil,
} from './resolve-common';
import vespersDefinitionData from '../data/hours/vespers.yml';
import vespersAntiphonsData from '../data/hours/vespers-antiphons.yml';
@@ -122,6 +123,10 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
{ kind: 'antiphon', text: full },
];
}
case 'vespers-preces': {
const id = isFerialOrVigil(day) ? 'vespers-preces-feriales' : 'lauds-short-litany';
return [{ kind: 'preces', text: resolveCommon(id) }];
}
case 'day-collects':
return getDayCollects(day);
case 'preces':
+32
View File
@@ -125,6 +125,38 @@ describe('resolveOrdo("vespers", ...)', () => {
expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Vexílla Regis pródeunt');
});
it('includes the Kyrie + Our Father lead-in right after the Magnificat, before the day collect (either form)', () => {
// 2026-06-16 is a Tuesday, plain ordinary trinitytide -- same bare
// ferial already used above for the Magnificat weekday-default test.
const ordo = resolveOrdo('vespers', '2026-06-16');
const litanyIndex = ordo.parts.findIndex((p) => p.kind === 'preces' && p.text.text.la?.includes('Kýrie, eléison'));
const magnificatIndex = ordo.parts.findIndex((p) => p.kind === 'canticle' && p.canticleId === 'magnificat');
const collectIndex = ordo.parts.findIndex((p) => p.kind === 'prayer');
expect(litanyIndex).toBeGreaterThan(magnificatIndex);
expect(collectIndex).toBeGreaterThan(litanyIndex);
});
it('uses the fuller Tridentine 1906/1910 ferial Preces (with Psalm 50) on a plain ferial day, not the short Sunday/feast litany', () => {
const ordo = resolveOrdo('vespers', '2026-06-16');
const preces = ordo.parts.find((p) => p.kind === 'preces' && p.text.text.la?.includes('Kýrie, eléison'));
expect(preces?.kind === 'preces' ? preces.text.text.la : undefined).toContain('Miserére mei, Deus');
expect(preces?.kind === 'preces' ? preces.text.text.en : undefined).toContain('Have mercy on me, O God');
});
it("uses the short Sunday/feast litany, without Psalm 50, on a Sunday and on a real saint's own winning day", () => {
// 2026-08-16 is a plain temporal Sunday -- no First Vespers claim from
// the following Monday (a plain feria), so this stays Sunday's own.
const sunday = resolveOrdo('vespers', '2026-08-16');
const sundayPreces = sunday.parts.find((p) => p.kind === 'preces' && p.text.text.la?.includes('Kýrie, eléison'));
expect(sundayPreces?.kind === 'preces' ? sundayPreces.text.text.la : undefined).not.toContain('Psalmus 50');
// 2026-08-15 is the Assumption (Duplex I. classis), a real saint's own
// Saturday -- same date already used above for the weekday-office test.
const saintsDay = resolveOrdo('vespers', '2026-08-15');
const saintsDayPreces = saintsDay.parts.find((p) => p.kind === 'preces' && p.text.text.la?.includes('Kýrie, eléison'));
expect(saintsDayPreces?.kind === 'preces' ? saintsDayPreces.text.text.la : undefined).not.toContain('Psalmus 50');
});
it("renders Paschaltide's seasonal office, sharing its chapter with Lauds", () => {
// 2026-04-20 is in eastertide (Easter 2026-04-05).
const ordo = resolveOrdo('vespers', '2026-04-20');