Fix Vespers ferial psalmody weekday bug, add duplex-majus+ Common psalmody override
Deploy / deploy (push) Successful in 1m33s
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:
@@ -48,6 +48,14 @@ across every hour and content type in this app.
|
||||
- The bulk Vulgate (Latin) + Douay-Rheims (English) scripture import into
|
||||
`src/data/scripture/` — long-term project, explicitly deferred; Matins scripture
|
||||
citations resolve to `missing` text until it lands.
|
||||
- Lauds/Vespers duplex-majus+ Common-category psalmody: 8 of 24 `SaintRecord.common`
|
||||
categories authored (covering 16 ids via aliases) for both hours — see "Lauds/Vespers
|
||||
duplex-majus+ Common-category psalmody" below. Remaining: `common-of-a-dedication`,
|
||||
`common-of-an-angel`, `common-of-an-evangelist`, `common-of-a-vigil`,
|
||||
`common-of-several-women-martyrs` (no votive code confirmed yet for any of these), plus
|
||||
Vespers' own per-feast **proper** tier (`data/hours/vespers-psalmody-overrides/saints/`)
|
||||
is mechanism-only, zero saints authored — unlike Lauds, no majus+ saint currently on the
|
||||
calendar is known to have proper Vespers psalm antiphons distinct from their Common's.
|
||||
|
||||
3. **Known, deliberate simplifications** — intentional, not bugs, no plan to revisit unless
|
||||
priorities change:
|
||||
@@ -3915,3 +3923,77 @@ separate `ordo-part` blocks that could be visually or structurally pulled apart.
|
||||
`tests/hours/matins.test.ts`'s Sunday proof (2026-09-06, Luke 7:11-16 + St. Augustine's homily)
|
||||
updated to assert against the new `gospel`/`homily` shape instead of two `isGospel`-flagged
|
||||
lessons. `npm test`/`npm run build` both pass.
|
||||
|
||||
### Vespers ferial psalmody weekday bug — fixed (2026-08-28)
|
||||
|
||||
Vespers psalms on Monday-Saturday were silently following the *anticipated* (First-Vespers)
|
||||
day's weekday instead of the actual calendar date's weekday, on any evening where tomorrow
|
||||
claimed First Vespers (`hasFirstVespers` has no rank floor, so this fired on most such
|
||||
evenings). Root cause: `hours/vespers.ts`'s `resolvePsalmody` reused `resolveEveningDay`'s
|
||||
result — correct for propers/office (chapter/responsory/hymn/versicle/collects/Magnificat
|
||||
antiphon), which legitimately borrow tomorrow's identity — but the ferial psalm-distribution
|
||||
table has no per-feast override of its own, so it needs today's own place in the fixed 6-day
|
||||
cycle regardless of whose office governs the evening. Fixed by computing `weekdayOf(date)`
|
||||
(the actual calendar weekday, `calendar/weekday.ts`) separately in `resolveOrdo` and threading
|
||||
it through as its own parameter, used only for the psalm-table lookup; `resolveOffice`'s
|
||||
ferial-bundle fallback correctly keeps using the governing (possibly-anticipated) day's
|
||||
weekday, since that IS a legitimate per-office-bundle rule. Regression test:
|
||||
`tests/hours/vespers.test.ts`, 2026-08-25 (Tuesday anticipating St. Zephyrinus's Aug 26 First
|
||||
Vespers) — expects Tuesday's psalm group `[129, 130, 131, 132]`, not Wednesday's. `npm
|
||||
test`/`npm run build` both pass.
|
||||
|
||||
### Lauds/Vespers duplex-majus+ Common-category psalmody — mechanism built, 8 categories authored (2026-08-28)
|
||||
|
||||
Prompted by a design question: does it make sense to gate a Vespers psalm override at
|
||||
duplex-majus+, the same threshold Matins already uses? Checked against the live reference
|
||||
engine rather than assuming, and found two real, previously-undiscovered facts:
|
||||
|
||||
- **Lauds already has this override** (`getPsalmodyOverrideFor`, duplex-majus+-gated), but only
|
||||
a per-feast tier (`lauds-psalmody-overrides/*.yml`) — no per-Common fallback, so most
|
||||
duplex-majus+ saints without their own authored proper fell straight through to the plain
|
||||
ferial default even though the real rubric calls for an override. Confirmed live
|
||||
(`votive=C1`..`C11`) that Lauds' override psalm *numbers* (92, 99, 62, Canticum Trium
|
||||
Puerorum) are identical across every Common — only the antiphons vary — matching the existing
|
||||
per-feast overrides' own doc comments (e.g. `st-lawrence.yml`'s note that its numbers "happen
|
||||
to be the same 92/99/62... that most Common-of-Martyrs-shaped feasts reuse").
|
||||
- **Vespers had no psalmody-override mechanism at all** (flagged honestly in its own doc
|
||||
comments as a known gap) — and unlike Lauds, its override psalm *numbers themselves* differ
|
||||
by Common (Common-of-an-Apostle: 109/112/115/138; Common-of-a-Confessor-Bishop:
|
||||
109/111/112/131; etc.), confirmed live the same way.
|
||||
|
||||
Mechanism, mirroring `matins-psalmody-overrides.ts`'s existing saint/category two-tier
|
||||
precedent: `hours/lauds-psalmody-overrides.ts` gained a `categories` tier (`data/hours/
|
||||
lauds-psalmody-overrides/categories/*.yml`, keyed by `SaintRecord.common` with optional
|
||||
`aliases`) consulted after the existing per-feast tier — `getLaudsPsalmodyOverride` now does
|
||||
`getLaudsSaintOverride(id) ?? getLaudsCommonOverride(id)`, so `hours/lauds.ts` needed no changes
|
||||
at all. `hours/vespers-psalmody-overrides.ts` is a new file built from scratch with the
|
||||
identical saint/category shape (the saint tier is mechanism-only, zero files yet), wired into
|
||||
`hours/vespers.ts`'s `resolvePsalmody` via the same `getOfficeOverrideId` eligibility test
|
||||
`resolveOfficeBundle` already uses for the chapter/hymn bundle — eligibility follows the
|
||||
evening's *governing* day (so a First Vespers of a majus+ feast gets that feast's own psalms,
|
||||
same as it gets that feast's own office), while the plain-ferial-fallback path still uses the
|
||||
actual calendar weekday (see the bug fix above).
|
||||
|
||||
**8 of 24 `SaintRecord.common` categories authored for both hours** (16 ids via aliases), each
|
||||
live-verified against Divinum Officium (Monastic Tridentinum 1617, `votive=C1`/`C2`/`C3`/`C4`/
|
||||
`C5`/`C6`/`C7`/`C11`, 2026-08-28): `common-of-an-apostle` (+`common-of-apostles`),
|
||||
`common-of-a-martyr` (+`common-of-a-martyr-bishop`), `common-of-several-martyrs`,
|
||||
`common-of-a-confessor-bishop` (+`common-of-a-confessor-doctor`),
|
||||
`common-of-a-confessor-not-bishop` (+`common-of-a-confessor`, `common-of-an-abbot`,
|
||||
`common-of-several-confessors`), `common-of-a-virgin` (+`common-of-a-virgin-martyr`),
|
||||
`common-of-a-holy-woman` (+`common-of-a-widow`), `common-of-the-bvm`. Remaining 5 categories
|
||||
(`common-of-a-dedication`, `common-of-an-angel`, `common-of-an-evangelist`,
|
||||
`common-of-a-vigil`, `common-of-several-women-martyrs`) have no confirmed votive code yet — see
|
||||
"Lauds/Vespers duplex-majus+ Common-category psalmody" in the Open-work list above.
|
||||
|
||||
Regression tests added to both `tests/hours/lauds.test.ts` and `tests/hours/vespers.test.ts`:
|
||||
a real category-tier date per hour (Our Lady of Mount Carmel/St. Benedict/Ss. Placid and
|
||||
Companions for Lauds; St. Bartholomew/St. Lawrence's octave/St. Martin of Tours's First Vespers
|
||||
for Vespers — several needed a different date than their existing office-bundle tests once the
|
||||
actual governing evening was traced through `resolveEveningDay`/octave substitution), plus one
|
||||
below-threshold (Semiduplex/Duplex) negative case per hour confirming the plain ferial default
|
||||
still wins there. One pre-existing Vespers test (`2026-08-17`, "joins Ps 115 and 116... on
|
||||
Monday") had to move to a different date: that Monday turned out to be the closing day of St.
|
||||
Lawrence's own octave, which now correctly picks up Common-of-a-Martyr's psalms instead of the
|
||||
plain ferial set it was (incidentally, not intentionally) relying on before this mechanism
|
||||
existed. `npm test` (535 passed) and `npm run build` both pass.
|
||||
|
||||
@@ -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."
|
||||
+36
@@ -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."
|
||||
+28
@@ -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."
|
||||
@@ -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 ?? '');
|
||||
}
|
||||
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -315,6 +315,52 @@ describe('resolveOrdo("lauds", ...)', () => {
|
||||
expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('O gloriósa Dómina');
|
||||
});
|
||||
|
||||
it("uses Common-of-the-BVM's own psalmody (92, 99, 62 antiphons + Trium Puerorum) on Our Lady of Mount Carmel's own day, not the ferial default", () => {
|
||||
// Live-verified against Divinum Officium (Monastic Tridentinum 1617,
|
||||
// votive=C11), 2026-08-28 -- see data/hours/lauds-psalmody-overrides/
|
||||
// categories/common-of-the-bvm.yml's own header. Same date as the
|
||||
// office-bundle test just above; she has no per-feast Lauds psalmody
|
||||
// override authored, so this is her Common's own tier.
|
||||
const ordo = resolveOrdo('lauds', '2026-07-16');
|
||||
const numbers = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
||||
expect(numbers).toEqual([66, 92, 99, 62, 148, 149, 150]);
|
||||
const canticle = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId === 'canticum-trium-puerorum');
|
||||
expect(canticle).toBeDefined();
|
||||
});
|
||||
|
||||
it("uses Common-of-an-Abbot's own psalmody on St. Benedict's own day, not the ferial default (his proper chapter/hymn is separate, see below)", () => {
|
||||
// St. Benedict (duplex-1-classis, common-of-an-abbot) has his own
|
||||
// proper chapter/hymn (see the test below) but no per-feast Lauds
|
||||
// psalmody override, so his psalm numbers/antiphons fall to his
|
||||
// Common's own tier -- live-verified against Divinum Officium
|
||||
// (Monastic Tridentinum 1617, votive=C5), 2026-08-28. Same clean year
|
||||
// as the proper-chapter test below.
|
||||
const ordo = resolveOrdo('lauds', '2033-03-21');
|
||||
const numbers = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
||||
expect(numbers).toEqual([66, 92, 99, 62, 148, 149, 150]);
|
||||
});
|
||||
|
||||
it("uses Common-of-Several-Martyrs' own psalmody on Ss. Placid and Companions' own day, not the ferial default", () => {
|
||||
// 2026-10-05, Duplex II. classis, common-of-several-martyrs, no
|
||||
// per-feast Lauds psalmody override authored -- same date as
|
||||
// vespers.test.ts's own office-bundle proof for this Common.
|
||||
// Live-verified against Divinum Officium (Monastic Tridentinum 1617,
|
||||
// votive=C3), 2026-08-28.
|
||||
const ordo = resolveOrdo('lauds', '2026-10-05');
|
||||
const numbers = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
||||
expect(numbers).toEqual([66, 92, 99, 62, 148, 149, 150]);
|
||||
});
|
||||
|
||||
it('stays on the plain ferial default for a below-threshold feast (Duplex), even with a Common that has its own category psalmody', () => {
|
||||
// St. Jerome (Sept 30, plain Duplex, common-of-a-confessor-not-bishop)
|
||||
// is below the duplex-majus+ override threshold -- confirmed live
|
||||
// against Divinum Officium, which keeps this date on the plain
|
||||
// ferial Wednesday set, not his Common's own psalmody.
|
||||
const ordo = resolveOrdo('lauds', '2026-09-30');
|
||||
const numbers = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
||||
expect(numbers).toEqual([66, 50, 63, 64, 148, 149, 150]);
|
||||
});
|
||||
|
||||
it("resolves St. Gregory the Great's own proper chapter/hymn, not a Common fallback", () => {
|
||||
// Live-verified against Divinum Officium (Monastic Tridentinum
|
||||
// 1617), 2033-03-12 (clean year). See lauds-capitulum-st-gregory-
|
||||
|
||||
@@ -75,8 +75,14 @@ describe('resolveOrdo("vespers", ...)', () => {
|
||||
});
|
||||
|
||||
it('joins Ps 115 and 116 under one shared antiphon on Monday', () => {
|
||||
// 2026-08-17 is a Monday, in ordinary time.
|
||||
const ordo = resolveOrdo('vespers', '2026-08-17');
|
||||
// Not 2026-08-17: that Monday is actually the closing day of St.
|
||||
// Lawrence's own octave (Aug 10-17, Duplex II. classis) -- now that
|
||||
// Vespers has its own duplex-majus+ psalmody override (2026-08-28),
|
||||
// that evening correctly gets Common-of-a-Martyr's own psalms
|
||||
// instead of the plain ferial Monday set, same as Lauds' own
|
||||
// octave-day override already did. 2026-09-28 is a Monday clear of
|
||||
// any such collision.
|
||||
const ordo = resolveOrdo('vespers', '2026-09-28');
|
||||
const psalmParts = ordo.parts.filter((p) => p.kind === 'psalm');
|
||||
const numbers = psalmParts.map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
||||
expect(numbers).toEqual([113, 114, 115, 116, 128]);
|
||||
@@ -107,6 +113,58 @@ describe('resolveOrdo("vespers", ...)', () => {
|
||||
expect(secondHalf?.kind === 'psalm' ? secondHalf.antiphon : undefined).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses Common-of-an-Apostle's own psalmody on St. Bartholomew's own day (Duplex II. classis, keeps own Vespers)", () => {
|
||||
// Live-verified against Divinum Officium (Monastic Tridentinum 1617,
|
||||
// votive=C1), 2026-08-28 -- see data/hours/vespers-psalmody-overrides/
|
||||
// categories/common-of-an-apostle.yml's own header. St. Bartholomew
|
||||
// has no proper Vespers psalmody of his own authored yet, so this is
|
||||
// his Common's own tier, not a per-feast override.
|
||||
const ordo = resolveOrdo('vespers', '2026-08-24');
|
||||
const numbers = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
||||
expect(numbers).toEqual([109, 112, 115, 138]);
|
||||
});
|
||||
|
||||
it("uses Common-of-a-Martyr's own psalmody on St. Lawrence's octave closing day, not the plain ferial Monday default", () => {
|
||||
// 2026-08-17 is the closing day of St. Lawrence's own octave (Aug
|
||||
// 10-17, Duplex II. classis) -- resolveOfficeWinner substitutes his
|
||||
// own identity there, so the new duplex-majus+ psalmody override
|
||||
// (getOfficeOverrideId) picks up his Common (common-of-a-martyr) even
|
||||
// though no per-feast Vespers override exists for him. See the
|
||||
// "joins Ps 115 and 116" test above for why this date was moved off
|
||||
// the plain-ferial-Monday proof.
|
||||
const ordo = resolveOrdo('vespers', '2026-08-17');
|
||||
const numbers = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
||||
expect(numbers).toEqual([109, 111, 112, 115]);
|
||||
});
|
||||
|
||||
it("uses Common-of-a-Confessor-Bishop's own psalmody for St. Martin of Tours's First Vespers", () => {
|
||||
// Not 2026-11-11 (his own day itself): St. Emilian (Nov 12, plain
|
||||
// Duplex, below this override's duplex-majus+ threshold) claims First
|
||||
// Vespers over Martin's own evening (Martin, Duplex Majus, falls
|
||||
// short of keepsOwnSecondVespers's duplex-2-classis+ floor) -- so
|
||||
// Nov 11's own evening resolves to Emilian's (below-threshold, plain
|
||||
// ferial) identity instead, not Martin's. 2026-11-10 anticipates
|
||||
// Martin's own First Vespers instead (Ss. Tryphon and Companions,
|
||||
// Nov 10, plain Simplex, doesn't keep its own evening either), so
|
||||
// that evening's governing day is Martin's, duplex-majus, eligible.
|
||||
// Live-verified against Divinum Officium (Monastic Tridentinum 1617,
|
||||
// votive=C4), 2026-08-28 -- see data/hours/vespers-psalmody-overrides/
|
||||
// categories/common-of-a-confessor-bishop.yml's own header.
|
||||
const ordo = resolveOrdo('vespers', '2026-11-10');
|
||||
const numbers = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
||||
expect(numbers).toEqual([109, 111, 112, 131]);
|
||||
});
|
||||
|
||||
it('stays on the plain ferial default for a below-threshold feast (Semiduplex), even on its own kept evening', () => {
|
||||
// 2026-08-08 (Ss. Cyriacus/Largus/Smaragdus, Semiduplex) is below the
|
||||
// duplex-majus+ override threshold -- confirmed live against Divinum
|
||||
// Officium, which also keeps this date on the plain ferial Saturday
|
||||
// set, not any Common's own psalmody.
|
||||
const ordo = resolveOrdo('vespers', '2026-08-08');
|
||||
const numbers = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
|
||||
expect(numbers).toEqual([144, 145, 146, 147]);
|
||||
});
|
||||
|
||||
it("uses the Assumption's own per-feast office override on her own Saturday, not the plain Saturday weekday default", () => {
|
||||
// 2026-08-15 is a Saturday and the Assumption (Duplex I. classis) —
|
||||
// well above keepsOwnSecondVespers's threshold, so it keeps its own
|
||||
|
||||
Reference in New Issue
Block a user