Fix Vespers ferial psalmody weekday bug, add duplex-majus+ Common psalmody override
Deploy / deploy (push) Successful in 1m33s

Vespers psalms on Mon-Sat were silently following the anticipated
First-Vespers day's weekday instead of the actual calendar date's, since
resolvePsalmody reused resolveEveningDay's result (correct for
propers/office, which legitimately borrow tomorrow's identity, but wrong
for the ferial psalm cycle, which has no per-feast override of its own).

Also adds a duplex-majus+ Common-category psalmody override to both
Lauds and Vespers, mirroring Matins' existing saint/category mechanism.
Lauds already had a per-feast-only override; Vespers had none. 8 of 24
Common categories authored for both hours, live-verified against the
reference engine.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHXJAWVAabCKX1JgmDh4Rn
This commit is contained in:
2026-08-28 20:51:06 -04:00
parent 25934865b1
commit 33c9b2415b
22 changed files with 835 additions and 21 deletions
@@ -0,0 +1,35 @@
# Generic Common-of-a-Confessor-Bishop Lauds psalmody, live-verified
# against Divinum Officium (Monastic Tridentinum 1617, command=prayLaudes,
# votive=C4). Psalm numbers (92, 99, 62, Canticum Trium Puerorum) are the
# same fixed Duplex-majus+ set used by every Common (see
# hours/lauds-psalmody-overrides.ts's own doc comment) -- only these
# antiphons are specific to this Common.
#
# `aliases` registers `common-of-a-confessor-doctor` under this same
# scheme -- votive=C4a at Lauds is byte-identical to C4 for these same
# slots (the only difference between C4 and C4a shows up in the
# Magnificat antiphon, handled separately, not this psalmody table).
id: common-of-a-confessor-bishop
aliases: [common-of-a-confessor-doctor]
groups:
- psalms: [92]
antiphon:
la: "Ecce sacérdos magnus, * qui in diébus suis plácuit Deo, et invéntus est iustus."
en: "Behold an high priest, * who in his days pleased God, and was found righteous."
- psalms: [99]
antiphon:
la: "Non est invéntus * símilis illi, qui conserváret legem Excélsi."
en: "None was found like unto him, * to keep the Law of the Most High."
- psalms: [62]
antiphon:
la: "Ideo iureiurándo * fecit illum Dóminus créscere in plebem suam."
en: "Therefore the Lord assured him * by an oath that He would multiply his seed among His people."
canticle:
id: canticum-trium-puerorum
antiphon:
la: "Sacerdótes Dei, * benedícite Dóminum: servi Dómini, hymnum dícite Deo. Allelúia."
en: "O all ye Priests of God, * bless ye the Lord O all ye servants of the Lord, sing praises unto our God."
laudate:
antiphon:
la: "Serve bone * et fidélis, intra in gáudium Dómini tui."
en: "Good and faithful servant, * enter thou into the joy of thy Lord."
@@ -0,0 +1,36 @@
# Generic Common-of-a-Confessor-Not-Bishop Lauds psalmody, live-verified
# against Divinum Officium (Monastic Tridentinum 1617, command=prayLaudes,
# votive=C5). Psalm numbers (92, 99, 62, Canticum Trium Puerorum) are the
# same fixed Duplex-majus+ set used by every Common (see
# hours/lauds-psalmody-overrides.ts's own doc comment) -- only these
# antiphons are specific to this Common.
#
# `aliases` registers `common-of-a-confessor`, `common-of-an-abbot`, and
# `common-of-several-confessors` under this same scheme -- all four ids'
# existing vespers-hymn-*.yml office-bundle files already cite the
# identical `votive=C5` source (see resolve-common.ts's resolveOfficeBundle
# header notes), so this is one scheme shared by four ids, not four.
id: common-of-a-confessor-not-bishop
aliases: [common-of-a-confessor, common-of-an-abbot, common-of-several-confessors]
groups:
- psalms: [92]
antiphon:
la: "Dómine, quinque talénta * tradidísti mihi, ecce ália quinque superlucrátus sum."
en: "Lord, Thou deliverest unto me five talents * behold, I have gained beside them five talents more."
- psalms: [99]
antiphon:
la: "Euge, serve bone * in módico fidélis, intra in gáudium Dómini tui."
en: "Well done, thou good servant, * thou hast been faithful in a very little, enter thou into the joy of thy Lord."
- psalms: [62]
antiphon:
la: "Fidélis servus * et prudens, quem constítuit Dóminus super famíliam suam."
en: "A faithful and wise servant * whom his Lord hath made ruler over His household."
canticle:
id: canticum-trium-puerorum
antiphon:
la: "Beátus ille servus, * quem, cum vénerit Dóminus eius et pulsáverit iánuam, invénerit vigilántem."
en: "Blessed is that servant * whom his Lord, when He cometh and knocketh at the door, shall find watching."
laudate:
antiphon:
la: "Serve bone * et fidélis, intra in gáudium Dómini tui."
en: "Thou good and faithful servant, * enter thou into the joy of thy Lord."
@@ -0,0 +1,35 @@
# Generic Common-of-a-Holy-Woman (Not-Virgin) Lauds psalmody, live-
# verified against Divinum Officium (Monastic Tridentinum 1617,
# command=prayLaudes, votive=C7). Psalm numbers (92, 99, 62, Canticum
# Trium Puerorum) are the same fixed Duplex-majus+ set used by every
# Common (see hours/lauds-psalmody-overrides.ts's own doc comment) --
# only these antiphons are specific to this Common.
#
# `aliases` registers `common-of-a-widow` under this same scheme --
# votive=C7 is the shared source for both ids' existing matins-hymn-*.yml
# office-bundle files (see matins-hymn-common-of-a-widow.yml's own
# header).
id: common-of-a-holy-woman
aliases: [common-of-a-widow]
groups:
- psalms: [92]
antiphon:
la: "Dum esset Rex * in accúbitu suo, nardus mea dedit odórem suavitátis."
en: "While the King sitteth at his table, * my spikenard sendeth forth the smell thereof."
- psalms: [99]
antiphon:
la: "In odórem * unguentórum tuórum cúrrimus: adolescéntulæ dilexérunt te nimis."
en: "We will run after thee, * in the savour of thy good ointments. The virgins love them exceedingly."
- psalms: [62]
antiphon:
la: "Iam hiems tránsiit, * imber ábiit et recéssit: surge, amíca mea, et veni."
en: "Lo the winter is past, * the rain is over and gone. Rise up, my love, and come away."
canticle:
id: canticum-trium-puerorum
antiphon:
la: "Veni, elécta mea, * et ponam in te thronum meum, allelúia."
en: "Come, O My chosen one, * and I will establish My throne in thee. Alleluia."
laudate:
antiphon:
la: "Ista est speciósa * inter fílias Ierúsalem."
en: "She is beautiful * among the daughters of Jerusalem."
@@ -0,0 +1,35 @@
# Generic Common-of-a-Martyr Lauds psalmody, live-verified against
# Divinum Officium (Monastic Tridentinum 1617, command=prayLaudes,
# votive=C2). Psalm numbers (92, 99, 62, Canticum Trium Puerorum) are the
# same fixed Duplex-majus+ set used by every Common (see
# hours/lauds-psalmody-overrides.ts's own doc comment) -- only these
# antiphons are Martyr-specific.
#
# `aliases` registers `common-of-a-martyr-bishop` under this same scheme
# -- votive=C2b at Lauds is byte-identical to C2 for these same slots
# (matches matins-psalmody-overrides/categories/common-of-a-martyr.yml's
# own finding that the two categories share one Matins scheme too).
id: common-of-a-martyr
aliases: [common-of-a-martyr-bishop]
groups:
- psalms: [92]
antiphon:
la: "Qui me conféssus fúerit * coram homínibus, confitébor et ego eum coram Patre meo."
en: "Whosoever shall confess Me * before men, him will I confess also before My Father."
- psalms: [99]
antiphon:
la: "Qui séquitur me, * non ámbulat in ténebris, sed habébit lumen vitæ, dicit Dóminus."
en: "He that followeth Me * walketh not in darkness, but shall have the light of life, saith the Lord."
- psalms: [62]
antiphon:
la: "Qui mihi minístrat, * me sequátur: et ubi ego sum, illic sit et miníster meus."
en: "If any man serve Me, * let him follow Me and where I am, there shall also My servant be."
canticle:
id: canticum-trium-puerorum
antiphon:
la: "Si quis mihi ministráverit, * honorificábit eum Pater meus, qui est in cælis, dicit Dóminus."
en: "If any man serve Me, * him will My Father, who is in heaven, honour."
laudate:
antiphon:
la: "Volo Pater, * ut ubi ego sum, illic sit et miníster meus."
en: "Father, I will * that where I am, there shall also My servant be."
@@ -0,0 +1,34 @@
# Generic Common-of-a-Virgin Lauds psalmody, live-verified against
# Divinum Officium (Monastic Tridentinum 1617, command=prayLaudes,
# votive=C6). Psalm numbers (92, 99, 62, Canticum Trium Puerorum) are the
# same fixed Duplex-majus+ set used by every Common (see
# hours/lauds-psalmody-overrides.ts's own doc comment) -- only these
# antiphons are specific to this Common.
#
# `aliases` registers `common-of-a-virgin-martyr` under this same scheme
# -- votive=C6 is the shared source for both ids' existing office-bundle
# files (see e.g. lauds-hymn-common-of-a-virgin-martyr.yml's own header).
id: common-of-a-virgin
aliases: [common-of-a-virgin-martyr]
groups:
- psalms: [92]
antiphon:
la: "Hæc est Virgo sápiens, * et una de número prudéntum."
en: "This is one of the wise virgins, * one chosen out of the number of the careful."
- psalms: [99]
antiphon:
la: "Hæc est Virgo sápiens, * quam Dóminus vigilántem invénit."
en: "This is one of the wise virgins, * whom the Lord found watching."
- psalms: [62]
antiphon:
la: "Hæc est quæ nescívit * torum in delícto: habébit fructum in respectióne animárum sanctárum."
en: "This, is one which hath not known the sinful bed, * she shall have fruit in the visitation of holy souls."
canticle:
id: canticum-trium-puerorum
antiphon:
la: "Veni, elécta mea, * et ponam in te thronum meum, allelúia."
en: "Come, O my chosen one, * and I will establish My throne in thee."
laudate:
antiphon:
la: "Ista est speciósa * inter fílias Ierúsalem."
en: "She is beautiful * among the daughters of Jerusalem."
@@ -0,0 +1,35 @@
# Generic Common-of-an-Apostle(s) Lauds psalmody, live-verified against
# Divinum Officium (Monastic Tridentinum 1617, command=prayLaudes,
# votive=C1). Psalm numbers (92, 99, 62, Canticum Trium Puerorum) are the
# same fixed Duplex-majus+ set used by every Common (see
# hours/lauds-psalmody-overrides.ts's own doc comment) -- only these
# antiphons are Apostles-specific.
#
# `aliases` registers `common-of-apostles` (the plural id used for a
# group of Apostles, e.g. Ss. Philip and James) under this same scheme --
# both ids' existing lauds-capitulum-*.yml office-bundle files already
# cite the identical `votive=C1` source, so this is one scheme, not two.
id: common-of-an-apostle
aliases: [common-of-apostles]
groups:
- psalms: [92]
antiphon:
la: "Hoc est præcéptum meum, * ut diligátis ínvicem, sicut diléxi vos."
en: "This is my commandment * that you love one another, as I have loved you."
- psalms: [99]
antiphon:
la: "Maiórem caritátem * nemo habet, ut ánimam suam ponat quis pro amícis suis."
en: "Greater love than this no man hath, * that a man lay down his life for his friends."
- psalms: [62]
antiphon:
la: "Vos amíci mei estis, * si fecéritis quæ præcípio vobis, dicit Dóminus."
en: "You are my friends * if you do the things that I command you, said the Lord."
canticle:
id: canticum-trium-puerorum
antiphon:
la: "Beáti pacífici, * beáti mundo corde: quóniam ipsi Deum vidébunt."
en: "Blessed are the peacemakers * the clean of heart, they shall see God."
laudate:
antiphon:
la: "In patiéntia vestra * possidébitis ánimas vestras."
en: "In your patience * you shall possess your souls."
@@ -0,0 +1,29 @@
# Generic Common-of-Several-Martyrs Lauds psalmody, live-verified against
# Divinum Officium (Monastic Tridentinum 1617, command=prayLaudes,
# votive=C3). Psalm numbers (92, 99, 62, Canticum Trium Puerorum) are the
# same fixed Duplex-majus+ set used by every Common (see
# hours/lauds-psalmody-overrides.ts's own doc comment) -- only these
# antiphons (all plural "Sancti"/"Martyres") are specific to this Common.
id: common-of-several-martyrs
groups:
- psalms: [92]
antiphon:
la: "Omnes Sancti, * quanta passi sunt torménta, ut secúri pervenírent ad palmam martýrii."
en: "O how many torments have all the Saints suffered, * that they might attain safely unto the palm of martyrdom."
- psalms: [99]
antiphon:
la: "Cum palma * ad regna pervenérunt Sancti, corónas decóris meruérunt de manu Dei."
en: "The Saints have attained unto the kingdom, * with palms in their hands; they have earned crowns of Majesty from the Lord's hand."
- psalms: [62]
antiphon:
la: "Córpora Sanctórum * in pace sepúlta sunt: et vivent nómina eórum in ætérnum."
en: "The bodies of the Saints are buried in peace, * and their name liveth for evermore."
canticle:
id: canticum-trium-puerorum
antiphon:
la: "Mártyres Dómini * Dóminum benedícite in ætérnum."
en: "O all ye Martyrs of the Lord, bless ye the Lord * for ever."
laudate:
antiphon:
la: "Mártyrum chorus * laudáte Dóminum de cælis, allelúia."
en: "O ye Martyrs, * praise ye the Lord from the heavens, praise Him with the dance, alleluia."
@@ -0,0 +1,29 @@
# Generic Common-of-the-BVM Lauds psalmody, live-verified against
# Divinum Officium (Monastic Tridentinum 1617, command=prayLaudes,
# votive=C11). Psalm numbers (92, 99, 62, Canticum Trium Puerorum) are
# the same fixed Duplex-majus+ set used by every Common (see
# hours/lauds-psalmody-overrides.ts's own doc comment) -- only these
# antiphons are specific to this Common.
id: common-of-the-bvm
groups:
- psalms: [92]
antiphon:
la: "Dum esset Rex * in accúbitu suo, nardus mea dedit odórem suavitátis."
en: "While the King sitteth at his table, * my spikenard sendeth forth the smell thereof."
- psalms: [99]
antiphon:
la: "Læva eius * sub cápite meo, et déxtera illíus amplexábitur me."
en: "His left hand is under my head, * and his right hand doth embrace me."
- psalms: [62]
antiphon:
la: "Nigra sum, * sed formósa, fíliæ Ierúsalem; ídeo diléxit me Rex, et introdúxit me in cubículum suum."
en: "I am black but comely, * O ye daughters of Jerusalem; therefore hath the King loved me, and brought me into his chamber."
canticle:
id: canticum-trium-puerorum
antiphon:
la: "Iam hiems tránsiit, * imber ábiit et recéssit: surge, amíca mea, et veni."
en: "Lo the winter is past, * the rain is over and gone. Rise up, my love, and come away."
laudate:
antiphon:
la: "Speciósa * facta es et suávis in delíciis tuis, sancta Dei Génetrix."
en: "O Holy Mother of God, thou art become beautiful * and gentle in thy gladness."
@@ -0,0 +1,28 @@
# Generic Common-of-a-Confessor-Bishop Vespers psalmody, live-verified
# against Divinum Officium (Monastic Tridentinum 1617, command=
# prayVespera, votive=C4, 2026-08-28).
#
# `aliases` registers `common-of-a-confessor-doctor` under this same
# scheme -- `votive=C4a` at Vespers returned byte-identical psalms/
# antiphons to `votive=C4` for these same four slots (only the
# Magnificat antiphon differs, handled separately, not this psalmody
# table).
id: common-of-a-confessor-bishop
aliases: [common-of-a-confessor-doctor]
groups:
- psalms: [109]
antiphon:
la: "Ecce sacérdos magnus, * qui in diébus suis plácuit Deo, et invéntus est iustus."
en: "Behold an high priest, * who in his days pleased God, and was found righteous."
- psalms: [111]
antiphon:
la: "Non est invéntus * símilis illi, qui conserváret legem Excélsi."
en: "None was found like unto him, * to keep the Law of the Most High."
- psalms: [112]
antiphon:
la: "Ideo iureiurándo * fecit illum Dóminus créscere in plebem suam."
en: "Therefore the Lord assured him * by an oath that He would multiply his seed among His people."
- psalms: [131]
antiphon:
la: "Serve bone * et fidélis, intra in gáudium Dómini tui."
en: "Good and faithful servant, * enter thou into the joy of thy Lord."
@@ -0,0 +1,28 @@
# Generic Common-of-a-Confessor-Not-Bishop Vespers psalmody, live-
# verified against Divinum Officium (Monastic Tridentinum 1617, command=
# prayVespera, votive=C5, 2026-08-28).
#
# `aliases` registers `common-of-a-confessor`, `common-of-an-abbot`, and
# `common-of-several-confessors` under this same scheme -- all four ids'
# existing vespers-hymn-*.yml office-bundle files already cite the
# identical `votive=C5` source (see resolve-common.ts's resolveOfficeBundle
# header notes), so this is one scheme shared by four ids, not four.
id: common-of-a-confessor-not-bishop
aliases: [common-of-a-confessor, common-of-an-abbot, common-of-several-confessors]
groups:
- psalms: [109]
antiphon:
la: "Dómine, quinque talénta * tradidísti mihi, ecce ália quinque superlucrátus sum."
en: "Lord, Thou deliverest unto me five talents * behold, I have gained beside them five talents more."
- psalms: [110]
antiphon:
la: "Euge, serve bone * in módico fidélis, intra in gáudium Dómini tui."
en: "Well done, thou good servant, * thou hast been faithful in a very little, enter thou into the joy of thy Lord."
- psalms: [111]
antiphon:
la: "Fidélis servus * et prudens, quem constítuit Dóminus super famíliam suam."
en: "A faithful and wise servant * whom his Lord hath made ruler over His household."
- psalms: [112]
antiphon:
la: "Serve bone * et fidélis, intra in gáudium Dómini tui."
en: "Thou good and faithful servant, * enter thou into the joy of thy Lord."
@@ -0,0 +1,30 @@
# Generic Common-of-a-Holy-Woman (Not-Virgin) Vespers psalmody, live-
# verified against Divinum Officium (Monastic Tridentinum 1617,
# command=prayVespera, votive=C7, 2026-08-28). Same four psalm numbers
# as Common-of-a-Virgin (109, 112, 121, 126) -- only the antiphons
# differ, so this is deliberately its own category file rather than an
# alias of common-of-a-virgin.
#
# `aliases` registers `common-of-a-widow` under this same scheme --
# votive=C7 is the shared source for both ids' existing matins-hymn-*.yml
# office-bundle files (see matins-hymn-common-of-a-widow.yml's own
# header).
id: common-of-a-holy-woman
aliases: [common-of-a-widow]
groups:
- psalms: [109]
antiphon:
la: "Dum esset Rex * in accúbitu suo, nardus mea dedit odórem suavitátis."
en: "While the King sitteth at his table, * my spikenard sendeth forth the smell thereof."
- psalms: [112]
antiphon:
la: "In odórem * unguentórum tuórum cúrrimus: adolescéntulæ dilexérunt te nimis."
en: "We will run after thee, * in the savour of thy good ointments. The virgins love them exceedingly."
- psalms: [121]
antiphon:
la: "Iam hiems tránsiit, * imber ábiit et recéssit: surge, amíca mea, et veni."
en: "Lo the winter is past, * the rain is over and gone. Rise up, my love, and come away."
- psalms: [126]
antiphon:
la: "Ista est speciósa * inter fílias Ierúsalem."
en: "She is beautiful * among the daughters of Jerusalem."
@@ -0,0 +1,27 @@
# Generic Common-of-a-Martyr Vespers psalmody, live-verified against
# Divinum Officium (Monastic Tridentinum 1617, command=prayVespera,
# votive=C2, 2026-08-28).
#
# `aliases` registers `common-of-a-martyr-bishop` under this same scheme
# -- `votive=C2b` at Vespers returned byte-identical psalms/antiphons to
# `votive=C2` for these same four slots (only the Magnificat antiphon
# differs, handled separately, not this psalmody table).
id: common-of-a-martyr
aliases: [common-of-a-martyr-bishop]
groups:
- psalms: [109]
antiphon:
la: "Qui me conféssus fúerit * coram homínibus, confitébor et ego eum coram Patre meo."
en: "Whosoever shall confess Me * before men, him will I confess also before My Father."
- psalms: [111]
antiphon:
la: "Qui séquitur me, * non ámbulat in ténebris, sed habébit lumen vitæ, dicit Dóminus."
en: "He that followeth Me * walketh not in darkness, but shall have the light of life, saith the Lord."
- psalms: [112]
antiphon:
la: "Qui mihi minístrat, * me sequátur: et ubi ego sum, illic sit et miníster meus."
en: "If any man serve Me, * let him follow Me and where I am, there shall also My servant be."
- psalms: [115]
antiphon:
la: "Volo Pater, * ut ubi ego sum, illic sit et miníster meus."
en: "Father, I will * that where I am, there shall also My servant be."
@@ -0,0 +1,27 @@
# Generic Common-of-a-Virgin Vespers psalmody, live-verified against
# Divinum Officium (Monastic Tridentinum 1617, command=prayVespera,
# votive=C6, 2026-08-28).
#
# `aliases` registers `common-of-a-virgin-martyr` under this same scheme
# -- votive=C6 is the shared source for both ids' existing office-bundle
# files (see e.g. vespers-hymn-common-of-a-virgin-martyr.yml's own
# header).
id: common-of-a-virgin
aliases: [common-of-a-virgin-martyr]
groups:
- psalms: [109]
antiphon:
la: "Hæc est Virgo sápiens, * et una de número prudéntum."
en: "This is one of the wise virgins, * one chosen out of the number of the careful."
- psalms: [112]
antiphon:
la: "Hæc est Virgo sápiens, * quam Dóminus vigilántem invénit."
en: "This is one of the wise virgins, * whom the Lord found watching."
- psalms: [121]
antiphon:
la: "Hæc est quæ nescívit * torum in delícto: habébit fructum in respectióne animárum sanctárum."
en: "This, is one which hath not known the sinful bed, * she shall have fruit in the visitation of holy souls."
- psalms: [126]
antiphon:
la: "Ista est speciósa * inter fílias Ierúsalem."
en: "She is beautiful * among the daughters of Jerusalem."
@@ -0,0 +1,27 @@
# Generic Common-of-an-Apostle(s) Vespers psalmody, live-verified against
# Divinum Officium (Monastic Tridentinum 1617, command=prayVespera,
# votive=C1, 2026-08-28).
#
# `aliases` registers `common-of-apostles` (the plural id used for a
# group of Apostles, e.g. Ss. Philip and James) under this same scheme --
# both ids' existing vespers-hymn-*.yml office-bundle files already cite
# the identical `votive=C1` source, so this is one scheme, not two.
id: common-of-an-apostle
aliases: [common-of-apostles]
groups:
- psalms: [109]
antiphon:
la: "Iurávit Dóminus, * et non pœnitébit eum: Tu es sacérdos in ætérnum."
en: "The Lord hath sworn * and will not repent: thou art a Priest for ever."
- psalms: [112]
antiphon:
la: "Cóllocet eum * Dóminus cum princípibus pópuli sui."
en: "That the Lord may set him * with the princes of his people."
- psalms: [115]
antiphon:
la: "Dirupísti, Dómine, * víncula mea: tibi sacrificábo hóstiam laudis."
en: "O Lord, thou hast loosed my bonds; * I will offer thee the sacrifice of thanksgiving."
- psalms: [138]
antiphon:
la: "Confortátus est * principátus eórum, et honoráti sunt amíci tui, Deus."
en: "O God, thy friends are honourable; * their power is waxen right strong."
@@ -0,0 +1,24 @@
# Generic Common-of-Several-Martyrs Vespers psalmody, live-verified
# against Divinum Officium (Monastic Tridentinum 1617, command=
# prayVespera, votive=C3, 2026-08-28). Same four psalm numbers as
# Common-of-a-Martyr (109, 111, 112, 115) -- only the antiphons (all
# plural "Sancti") differ, so this is deliberately its own category file
# rather than an alias of common-of-a-martyr.
id: common-of-several-martyrs
groups:
- psalms: [109]
antiphon:
la: "Isti sunt Sancti, * qui pro testaménto Dei sua córpora tradidérunt, et in sánguine Agni lavérunt stolas suas."
en: "These men are holy, * for they have given up their bodies unto death for the sake of the covenant of their God, and have washed their robes in the Blood of the Lamb."
- psalms: [111]
antiphon:
la: "Sancti per fidem * vicérunt regna, operáti sunt iustítiam, adépti sunt repromissiónes."
en: "The Saints through faith subdued kingdoms, * wrought righteousness, obtained promises."
- psalms: [112]
antiphon:
la: "Sanctórum velut áquilæ * iuvéntus renovábitur: florébunt sicut lílium in civitáte Dómini."
en: "The youth of the Saints shall be renewed * like the eagle's they shall grow as the lily in the city of the Lord."
- psalms: [115]
antiphon:
la: "In cæléstibus regnis * Sanctórum habitátio est, et in ætérnum réquies eórum."
en: "In the heavenly kingdoms, * there is the dwelling of the Saints there shall be their rest for ever and ever."
@@ -0,0 +1,24 @@
# Generic Common-of-the-BVM Vespers psalmody, live-verified against
# Divinum Officium (Monastic Tridentinum 1617, command=prayVespera,
# votive=C11, 2026-08-28). Same four psalm numbers as Common-of-a-Virgin/
# Common-of-a-Holy-Woman (109, 112, 121, 126) -- only the antiphons
# differ, so this is deliberately its own category file rather than an
# alias.
id: common-of-the-bvm
groups:
- psalms: [109]
antiphon:
la: "Dum esset Rex * in accúbitu suo, nardus mea dedit odórem suavitátis."
en: "While the King sitteth at his table, * my spikenard sendeth forth the smell thereof."
- psalms: [112]
antiphon:
la: "Læva eius * sub cápite meo, et déxtera illíus amplexábitur me."
en: "His left hand is under my head, * and his right hand doth embrace me."
- psalms: [121]
antiphon:
la: "Nigra sum, * sed formósa, fíliæ Ierúsalem; ídeo diléxit me Rex, et introdúxit me in cubículum suum."
en: "I am black but comely, * O ye daughters of Jerusalem; therefore hath the King loved me, and brought me into his chamber."
- psalms: [126]
antiphon:
la: "Speciósa * facta es et suávis in delíciis tuis, sancta Dei Génetrix."
en: "O Holy Mother of God, thou art become beautiful * and gentle in thy gladness."
+57 -9
View File
@@ -1,13 +1,33 @@
import { getSaintRecord } from '../calendar/feasts';
/** Same shape as data/hours/lauds-antiphons.yml's per-weekday entries
* (see hours/lauds.ts) — a whole-block substitute for the plain ferial
* weekday psalmody, keyed by id: either a saint's own id (gated at
* duplex-majus+, see hours/lauds.ts's getPsalmodyOverrideFor) or the
* literal 'marian-saturday' (applies unconditionally whenever it wins).
* Deliberately per-feast, not per-Common — explicit choice, even though
* it means most duplex-majus+ saints across the year don't have one of
* these authored yet and fall back to the plain weekday default until
* they do (same "mechanism first, content incrementally" pattern as
* everywhere else — not a bug, an expected, honest gap). */
* weekday psalmody. Two tiers, consulted in order by
* `getLaudsPsalmodyOverride` (mirrors matins-psalmody-overrides.ts's own
* saint/category split):
*
* 1. **Proper** — `data/hours/lauds-psalmody-overrides/*.yml`, keyed by a
* saint's own id (gated at duplex-majus+, see hours/lauds.ts's
* getPsalmodyOverrideFor) or the literal 'marian-saturday' (applies
* unconditionally whenever it wins) — real proper antiphon text for a
* saint who has one, live-verified.
* 2. **Common** — `data/hours/lauds-psalmody-overrides/categories/*.yml`,
* one file per `SaintRecord.common` category (with optional `aliases`
* for a second category id sharing the identical scheme, e.g. Common-
* of-a-Confessor-Doctor reuses Common-of-a-Confessor-Bishop's Lauds
* psalmody verbatim in the reference engine). Live-verified fact
* (2026-08-28, `votive=C1`..`C11` queries): the psalm *numbers* here
* (92, 99, 62, Canticum Trium Puerorum) are the same for every
* Common — only the antiphons vary by category — which is exactly why
* the existing per-feast overrides above (e.g. st-lawrence.yml) already
* reuse those same three numbers with their own proper antiphons; the
* category tier is just the generic version of that when no per-feast
* proper has been authored yet.
*
* Falls through to the plain weekday default (data/hours/lauds-
* antiphons.yml) when neither tier has anything for this id — same
* "mechanism first, content incrementally" pattern as everywhere else,
* not a bug. */
export interface LaudsPsalmodyOverride {
id: string;
groups: { psalms: number[]; antiphon: Partial<Record<string, string>> }[];
@@ -16,17 +36,45 @@ export interface LaudsPsalmodyOverride {
canticle: { id: string; antiphon: Partial<Record<string, string>>; split?: number };
laudate: { antiphon: Partial<Record<string, string>> };
}
export interface LaudsPsalmodyCategory extends LaudsPsalmodyOverride {
aliases?: string[];
}
const modules = import.meta.glob<{ default: LaudsPsalmodyOverride }>(
'../data/hours/lauds-psalmody-overrides/*.yml',
{ eager: true },
);
const categoryModules = import.meta.glob<{ default: LaudsPsalmodyCategory }>(
'../data/hours/lauds-psalmody-overrides/categories/*.yml',
{ eager: true },
);
const overrides = new Map<string, LaudsPsalmodyOverride>();
for (const mod of Object.values(modules)) {
overrides.set(mod.default.id, mod.default);
}
export function getLaudsPsalmodyOverride(id: string): LaudsPsalmodyOverride | undefined {
const categories = new Map<string, LaudsPsalmodyCategory>();
for (const mod of Object.values(categoryModules)) {
categories.set(mod.default.id, mod.default);
for (const alias of mod.default.aliases ?? []) {
categories.set(alias, mod.default);
}
}
export function getLaudsPsalmodyOverride(id: string | undefined): LaudsPsalmodyOverride | undefined {
if (!id) return undefined;
return getLaudsSaintOverride(id) ?? getLaudsCommonOverride(id);
}
/** Saint-specific proper only, skipping the Common-category tier. */
export function getLaudsSaintOverride(id: string | undefined): LaudsPsalmodyOverride | undefined {
if (!id) return undefined;
return overrides.get(id);
}
/** Common-category lookup only, joining via `SaintRecord.common`. */
export function getLaudsCommonOverride(id: string | undefined): LaudsPsalmodyOverride | undefined {
if (!id) return undefined;
return categories.get(getSaintRecord(id)?.common ?? '');
}
+79
View File
@@ -0,0 +1,79 @@
import { getSaintRecord } from '../calendar/feasts';
/** Vespers' own analogue of hours/lauds-psalmody-overrides.ts (and, more
* distantly, matins-psalmody-overrides.ts) — a whole-block substitute for
* the plain ferial weekday psalmody (data/hours/vespers-antiphons.yml),
* gated the same duplex-majus+ way as Lauds' own override
* (hours/vespers.ts's getPsalmodyOverrideFor, reusing resolve-common.ts's
* getOfficeOverrideId). Two tiers, consulted in order by
* `getVespersPsalmodyOverride`:
*
* 1. **Proper** — `data/hours/vespers-psalmody-overrides/saints/*.yml`,
* a specific saint's own proper Vespers psalmody, keyed by saint id.
* None authored yet (no duplex-majus+ saint in this app currently has
* proper Vespers psalm antiphons distinct from their Common's) — the
* tier exists so one can be added later without a mechanism change,
* same "mechanism first, content incrementally" pattern as Lauds'.
* 2. **Common** — `data/hours/vespers-psalmody-overrides/categories/
* *.yml`, one file per `SaintRecord.common` category (with optional
* `aliases` for a second category id sharing the identical scheme).
* Live-verified (2026-08-28, `votive=C1`..`C11` queries against
* Divinum Officium, Monastic Tridentinum 1617): unlike Lauds (whose
* psalm *numbers* are the same fixed set for every Common), Vespers'
* psalm numbers genuinely differ by Common — e.g. Common-of-an-Apostle
* is 109/112/115/138, Common-of-a-Confessor-Bishop is 109/111/112/131
* — so this tier carries its own `groups` per category, not just
* antiphons layered onto one shared number set.
*
* Falls through to the plain weekday default (data/hours/vespers-
* antiphons.yml) when neither tier has anything for this id. */
export interface VespersPsalmodyGroup {
psalms: (number | { number: number; verses?: string })[];
antiphon: Partial<Record<string, string>>;
}
export interface VespersPsalmodyOverride {
id: string;
groups: VespersPsalmodyGroup[];
}
export interface VespersPsalmodyCategory extends VespersPsalmodyOverride {
aliases?: string[];
}
const saintModules = import.meta.glob<{ default: VespersPsalmodyOverride }>(
'../data/hours/vespers-psalmody-overrides/saints/*.yml',
{ eager: true },
);
const categoryModules = import.meta.glob<{ default: VespersPsalmodyCategory }>(
'../data/hours/vespers-psalmody-overrides/categories/*.yml',
{ eager: true },
);
const overrides = new Map<string, VespersPsalmodyOverride>();
for (const mod of Object.values(saintModules)) {
overrides.set(mod.default.id, mod.default);
}
const categories = new Map<string, VespersPsalmodyCategory>();
for (const mod of Object.values(categoryModules)) {
categories.set(mod.default.id, mod.default);
for (const alias of mod.default.aliases ?? []) {
categories.set(alias, mod.default);
}
}
export function getVespersPsalmodyOverride(id: string | undefined): VespersPsalmodyOverride | undefined {
if (!id) return undefined;
return getVespersSaintOverride(id) ?? getVespersCommonOverride(id);
}
/** Saint-specific proper only, skipping the Common-category tier. */
export function getVespersSaintOverride(id: string | undefined): VespersPsalmodyOverride | undefined {
if (!id) return undefined;
return overrides.get(id);
}
/** Common-category lookup only, joining via `SaintRecord.common`. */
export function getVespersCommonOverride(id: string | undefined): VespersPsalmodyOverride | undefined {
if (!id) return undefined;
return categories.get(getSaintRecord(id)?.common ?? '');
}
+28 -10
View File
@@ -5,6 +5,7 @@ import { weekdayOf } from '../calendar/weekday';
import { getDayLabel } from '../calendar/day-label';
import { getPsalmVerses } from '../psalter';
import { getOpeningVersicleId } from './opening-versicle';
import { getVespersPsalmodyOverride } from './vespers-psalmody-overrides';
import { applyFlexaMark } from './antiphon';
import {
resolveCommon,
@@ -16,6 +17,7 @@ import {
verifiedText,
seasonalOfficeSuffix,
isFerialOrVigil,
getOfficeOverrideId,
resolveResponsory,
resolveOfficeBundle,
resolveSuffrages,
@@ -68,19 +70,35 @@ function psalmParts(group: VespersGroup, day: LiturgicalDay): ResolvedPart[] {
return parts;
}
/** A duplex-majus+ saint's own proper Vespers psalmody (none authored
* yet), or their Common's (live-verified 2026-08-28, see
* hours/vespers-psalmody-overrides.ts's own doc comment for why this
* mechanism is genuinely per-Common — unlike Lauds, the psalm *numbers*
* themselves differ by Common here, not just the antiphons). Eligibility
* follows the evening's governing `day` (same getOfficeOverrideId test
* as the chapter/responsory/hymn/versicle bundle below) — a First
* Vespers of a majus+ feast should get that feast's own psalms, same as
* it gets that feast's own office. Falls back to `undefined` for an
* eligible feast with no override authored yet, same honest incremental-
* content convention as everywhere else. */
function getPsalmodyOverrideFor(day: LiturgicalDay): VespersDay | undefined {
const id = getOfficeOverrideId(day);
return id ? getVespersPsalmodyOverride(id) : undefined;
}
/** The 4 weekday-variable psalm groups — no fixed leading/trailing psalm
* the way Lauds has (Ps 66 / the Laudate psalms); Vespers is just these 4
* groups in sequence. No per-feast override mechanism yet — see
* hours/types.ts's 'vespers-psalmody' doc comment. Deliberately keyed off
* `weekday` (today's actual calendar weekday), not `day.weekday` — `day`
* may be tomorrow's identity when tonight anticipates First Vespers (see
* resolveEveningDay), but the psalm cycle has no per-feast override and
* must stay on today's place in the fixed 6-day rotation regardless of
* whose office governs the evening's propers. `day` is still threaded
* into psalmParts for antiphon-fullness resolution, which does care who
* governs tonight. */
* groups in sequence, substituted wholesale by getPsalmodyOverrideFor
* above when eligible. Deliberately keyed off `weekday` (today's actual
* calendar weekday), not `day.weekday`, for the plain-ferial-fallback
* case — `day` may be tomorrow's identity when tonight anticipates First
* Vespers (see resolveEveningDay), but the ferial psalm cycle has no
* per-feast override of its own and must stay on today's place in the
* fixed 6-day rotation regardless of whose office governs the evening's
* propers. `day` is still threaded into psalmParts for antiphon-fullness
* resolution, which does care who governs tonight. */
function resolvePsalmody(day: LiturgicalDay, weekday: Weekday): ResolvedPart[] {
const wd = vespersAntiphons[weekday];
const wd = getPsalmodyOverrideFor(day) ?? vespersAntiphons[weekday];
return wd.groups.flatMap((group) => psalmParts(group, day));
}