From de83786793fbb4496279d544d4ae3dac87c7064d Mon Sep 17 00:00:00 2001 From: Will Estes Date: Thu, 13 Aug 2026 20:14:19 -0400 Subject: [PATCH] Build the Vespers ordo: mechanism, weekday content, seasonal content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires resolveOrdo('vespers', ...) end to end: opening versicle, weekday psalmody (4 groups, handling the joined Ps 115+116 and the split Ps 138/143 via a PsalmRef-like shape), the chapter/responsory/ hymn/versicle office bundle, Magnificat, day-collects — reusing resolveEveningDay so First/Second Vespers precedence and the cross-day commemoration (both added in the previous commit) apply automatically — and a closing that reuses Lauds' own conclusio, since no distinct "Vespera" version of that exact text turned up in the source. getMagnificatAntiphon mirrors getBenedictusAntiphon's saint/temporal-id lookup order, but also falls back to real content Benedictus has no equivalent of: the classic ferial set that quotes the Magnificat's own text in sequence across the week, with the correct Monastic-rubric variant picked for Tue/Thu/Fri/Sat (two of those needed a fresh translation where the Monastic Latin diverges from the given English in content, not just wording). Also includes Vespers' own seasonal content (Advent/Lent/Passiontide/ Paschaltide chapter+responsory+hymn+versicle), transcribed from the same source blocks as the weekday content, with the Monastic-specific hymn variants hand-applied from the source's substitution notation against each base Roman text (one transcription slip caught and fixed mid-pass: the Passiontide hymn's closing lines had been copied from the Roman text instead of the Monastic one actually being transcribed). Passiontide's own responsory/hymn correctly omit the Gloria Patri, matching real rubric and the source data as given. All weekday/seasonal content marked `draft`, not `verified` — read directly from the reference source, not cross-checked against a live rendering per day the way the psalmody was. Six other hours' resolvePart switches needed the three new HourPart kinds ('vespers-psalmody', 'vespers-office', 'magnificat') added to their exhaustive not-used lists so they still typecheck; three pre-existing tests assumed Vespers was still "coming soon" and are updated to exercise Matins (still the genuine stub) instead. Co-Authored-By: Claude Sonnet 5 --- .../hours/vespers-magnificat-antiphons.yml | 64 +++++++ src/data/hours/vespers.yml | 23 +++ src/data/propers/common/magnificat.yml | 38 ++++ .../common/vespers-capitulum-advent.yml | 13 ++ .../propers/common/vespers-capitulum-lent.yml | 13 ++ .../common/vespers-capitulum-paschaltide.yml | 16 ++ .../common/vespers-capitulum-passiontide.yml | 13 ++ .../common/vespers-capitulum-saturday.yml | 18 ++ .../vespers-capitulum-sunday-ferial.yml | 17 ++ .../propers/common/vespers-hymn-advent.yml | 78 ++++++++ .../propers/common/vespers-hymn-friday.yml | 63 +++++++ src/data/propers/common/vespers-hymn-lent.yml | 63 +++++++ .../propers/common/vespers-hymn-monday.yml | 62 +++++++ .../common/vespers-hymn-paschaltide.yml | 96 ++++++++++ .../common/vespers-hymn-passiontide.yml | 93 ++++++++++ .../propers/common/vespers-hymn-saturday.yml | 43 +++++ .../propers/common/vespers-hymn-sunday.yml | 65 +++++++ .../propers/common/vespers-hymn-thursday.yml | 64 +++++++ .../propers/common/vespers-hymn-tuesday.yml | 64 +++++++ .../propers/common/vespers-hymn-wednesday.yml | 64 +++++++ .../common/vespers-responsory-advent.yml | 23 +++ .../common/vespers-responsory-ferial.yml | 23 +++ .../common/vespers-responsory-lent.yml | 22 +++ .../common/vespers-responsory-paschaltide.yml | 25 +++ .../common/vespers-responsory-passiontide.yml | 21 +++ .../common/vespers-responsory-saturday.yml | 22 +++ .../common/vespers-responsory-sunday.yml | 22 +++ .../common/vespers-versicle-advent.yml | 15 ++ .../propers/common/vespers-versicle-lent.yml | 17 ++ .../common/vespers-versicle-paschaltide.yml | 16 ++ .../common/vespers-versicle-passiontide.yml | 16 ++ .../common/vespers-versicle-saturday.yml | 16 ++ .../common/vespers-versicle-sunday-ferial.yml | 16 ++ src/hours/compline.ts | 3 + src/hours/lauds.ts | 3 + src/hours/none.ts | 3 + src/hours/prime.ts | 3 + src/hours/resolve-common.ts | 76 +++++++- src/hours/sext.ts | 3 + src/hours/terce.ts | 3 + src/hours/types.ts | 23 +++ src/hours/vespers.ts | 170 +++++++++++++++++- tests/hours/prime.test.ts | 4 +- tests/hours/vespers.test.ts | 138 ++++++++++++++ tests/ui/shell.test.ts | 8 +- 45 files changed, 1649 insertions(+), 12 deletions(-) create mode 100644 src/data/hours/vespers-magnificat-antiphons.yml create mode 100644 src/data/hours/vespers.yml create mode 100644 src/data/propers/common/magnificat.yml create mode 100644 src/data/propers/common/vespers-capitulum-advent.yml create mode 100644 src/data/propers/common/vespers-capitulum-lent.yml create mode 100644 src/data/propers/common/vespers-capitulum-paschaltide.yml create mode 100644 src/data/propers/common/vespers-capitulum-passiontide.yml create mode 100644 src/data/propers/common/vespers-capitulum-saturday.yml create mode 100644 src/data/propers/common/vespers-capitulum-sunday-ferial.yml create mode 100644 src/data/propers/common/vespers-hymn-advent.yml create mode 100644 src/data/propers/common/vespers-hymn-friday.yml create mode 100644 src/data/propers/common/vespers-hymn-lent.yml create mode 100644 src/data/propers/common/vespers-hymn-monday.yml create mode 100644 src/data/propers/common/vespers-hymn-paschaltide.yml create mode 100644 src/data/propers/common/vespers-hymn-passiontide.yml create mode 100644 src/data/propers/common/vespers-hymn-saturday.yml create mode 100644 src/data/propers/common/vespers-hymn-sunday.yml create mode 100644 src/data/propers/common/vespers-hymn-thursday.yml create mode 100644 src/data/propers/common/vespers-hymn-tuesday.yml create mode 100644 src/data/propers/common/vespers-hymn-wednesday.yml create mode 100644 src/data/propers/common/vespers-responsory-advent.yml create mode 100644 src/data/propers/common/vespers-responsory-ferial.yml create mode 100644 src/data/propers/common/vespers-responsory-lent.yml create mode 100644 src/data/propers/common/vespers-responsory-paschaltide.yml create mode 100644 src/data/propers/common/vespers-responsory-passiontide.yml create mode 100644 src/data/propers/common/vespers-responsory-saturday.yml create mode 100644 src/data/propers/common/vespers-responsory-sunday.yml create mode 100644 src/data/propers/common/vespers-versicle-advent.yml create mode 100644 src/data/propers/common/vespers-versicle-lent.yml create mode 100644 src/data/propers/common/vespers-versicle-paschaltide.yml create mode 100644 src/data/propers/common/vespers-versicle-passiontide.yml create mode 100644 src/data/propers/common/vespers-versicle-saturday.yml create mode 100644 src/data/propers/common/vespers-versicle-sunday-ferial.yml create mode 100644 tests/hours/vespers.test.ts diff --git a/src/data/hours/vespers-magnificat-antiphons.yml b/src/data/hours/vespers-magnificat-antiphons.yml new file mode 100644 index 0000000..fe035c8 --- /dev/null +++ b/src/data/hours/vespers-magnificat-antiphons.yml @@ -0,0 +1,64 @@ +# The classic ferial Magnificat-antiphon set: each weekday quotes the +# Magnificat's own text in sequence (Monday = its opening verse, Tuesday = +# the next, and so on through Saturday). Sourced from Divinum Officium's +# Monastic block ("[Feria2 Ant 3]" through "[Feria7 Ant 3]", Psalterium/ +# Special/Major Special.txt). Sunday has no entry here on purpose — its own +# source ("[Dominica Ant 3] = @:Dominica Ant 2 = /:ut in Proprio de +# Tempore:/") draws from that specific Sunday's own Proper of the Time, not +# a fixed weekly default; unauthored content for now (see +# hours/resolve-common.ts's getMagnificatAntiphon). +# +# Four of the six weekdays have a real Monastic-specific Latin variant, +# marked in the source "(sed rubrica tridentina aut rubrica monastica)" — +# used here in place of the plain/Roman line above it, per direct +# instruction to read this app's target rubric (pre-1955, Monastic +# Tridentinum 1617) straight from these branch labels rather than treat +# them as optional. The English source only tracks that same variant for +# Saturday; Tuesday's only differs by a verb ending (no translation +# impact), but Thursday's and Friday's Monastic Latin diverge from the +# English enough in *content* (not just wording) that the given English +# would misrepresent them — those two are translated fresh here instead, +# marked `draft` for that reason specifically, not just the general +# not-live-verified caveat the rest of this file carries. +# +# Not live-query-verified (see TODO.md) — read directly from source. + +monday: + antiphon: + la: "Magníficat * ánima mea Dóminum, quia respéxit Deus humilitátem meam." + en: "My soul * magnifies the Lord, because God has regarded my lowliness." + +tuesday: + antiphon: + la: "Exsúltet * spíritus meus in Deo salutári meo." + en: "My spirit rejoices * in God, in my Saviour." + +wednesday: + antiphon: + la: "Respéxit Dóminus * humilitátem meam, et fecit in me magna, qui potens est." + en: "The Lord has regarded * my lowliness, and he who is mighty has done great things through me." + +thursday: + # Monastic Latin diverges from the given (Roman-track) English in mood + # and content — translated fresh to match it; see file header. `status` + # marks the translation itself draft, distinct from the rest of this + # file's blanket "not live-verified" caveat. + antiphon: + la: "Fac Deus, * poténtiam in brácchio tuo: dispérde supérbos, et exálta húmiles." + en: "Shew, O God, * power in thy arm: scatter the proud, and exalt the humble." + status: { la: draft, en: draft } + +friday: + # Monastic Latin diverges from the given (Roman-track) English in + # content — translated fresh to match it; see file header. `status` + # marks the translation itself draft, distinct from the rest of this + # file's blanket "not live-verified" caveat. + antiphon: + la: "Depósuit poténtes, * Sanctos persequéntes: et exaltávit húmiles, Christum confiténtes." + en: "He hath put down the mighty, * who persecuted the Saints: and exalted the humble, who confessed Christ." + status: { la: draft, en: draft } + +saturday: + antiphon: + la: "Suscépit Deus * Israël, púerum suum: sicut locútus est ad Ábraham, et semen ejus: exaltáre húmiles usque in sǽculum." + en: "God hath received * Israel his servant: as he spoke to our fathers, to Abraham and to his seed, and hath exalted the humble forever." diff --git a/src/data/hours/vespers.yml b/src/data/hours/vespers.yml new file mode 100644 index 0000000..3c8dd53 --- /dev/null +++ b/src/data/hours/vespers.yml @@ -0,0 +1,23 @@ +# Ordo assembled from Divinum Officium's Monastic block (see the +# individual data/propers/common/vespers-*.yml and data/hours/vespers- +# antiphons.yml/vespers-magnificat-antiphons.yml files for sourcing) — not +# live-query-verified end-to-end (see TODO.md). +# +# Resolves off resolveEveningDay, not resolveDay — see hours/vespers.ts — +# so First/Second Vespers precedence and the cross-day commemoration both +# apply before any part below ever runs. +# +# The closing reuses `lauds-conclusio` (Benedicamus Domino / Fidelium +# animae) rather than a Vespers-specific id — no distinct "Vespera" +# version of this exact closing turned up in the source, and it's the +# same fixed formula in real practice after either hour; flagged here in +# case that assumption turns out wrong. +id: vespers +parts: + - kind: opening-versicle + - kind: vespers-psalmody + - kind: vespers-office + - kind: magnificat + - kind: day-collects + - kind: preces + textRef: { source: common, id: lauds-conclusio } diff --git a/src/data/propers/common/magnificat.yml b/src/data/propers/common/magnificat.yml new file mode 100644 index 0000000..1ddcc82 --- /dev/null +++ b/src/data/propers/common/magnificat.yml @@ -0,0 +1,38 @@ +# Sourced from Divinum Officium's own canticle text (Latin/English +# Psalterium/Psalmorum/Psalm232.txt — the Magnificat, Luke 1:46-55, stored +# alongside the other canticles rather than the numbered psalms). Fixed +# year-round, same shape as benedictus.yml/nunc-dimittis.yml — the framing +# antiphon is resolved separately, by day (see hours/resolve-common.ts's +# getMagnificatAntiphon), not stored here. Not live-query-verified (see +# TODO.md) — read directly from source. +id: magnificat +text: + la: | + Magníficat ✠ ánima mea Dóminum. + Et exsultávit spíritus meus: * in Deo, salutári meo. + Quia respéxit humilitátem ancíllæ suæ: * ecce enim ex hoc beátam me dicent omnes generatiónes. + Quia fecit mihi magna qui potens est: * et sanctum nomen ejus. + Et misericórdia ejus, a progénie in progénies: * timéntibus eum. + Fecit poténtiam in brácchio suo: * dispérsit supérbos mente cordis sui. + Depósuit poténtes de sede: * et exaltávit húmiles. + Esuriéntes implévit bonis: * et dívites dimísit inánes. + Suscépit Israël púerum suum: * recordátus misericórdiæ suæ. + Sicut locútus est ad patres nostros: * Ábraham, et sémini ejus in sǽcula. + V. Glória Patri, et Fílio, * et Spirítui Sancto. + R. Sicut erat in princípio, et nunc, et semper, * et in sǽcula sæculórum. Amen. + en: | + My soul ✠ doth magnify the Lord. + And my spirit hath rejoiced * in God my Saviour. + Because he hath regarded the humility of his handmaid; * for behold from henceforth all generations shall call me blessed. + Because he that is mighty, hath done great things to me; * and holy is his name. + And his mercy is from generation unto generations, * to them that fear him. + He hath shewed might in his arm: * he hath scattered the proud in the conceit of their heart. + He hath put down the mighty from their seat, * and hath exalted the humble. + He hath filled the hungry with good things; * and the rich he hath sent empty away. + He hath received Israel his servant, * being mindful of his mercy: + As he spoke to our fathers, * to Abraham and to his seed for ever. + V. Glory be to the Father, and to the Son, * and to the Holy Ghost. + R. As it was in the beginning, is now, * and ever shall be, world without end. Amen. +status: + la: verified + en: verified diff --git a/src/data/propers/common/vespers-capitulum-advent.yml b/src/data/propers/common/vespers-capitulum-advent.yml new file mode 100644 index 0000000..f3629f3 --- /dev/null +++ b/src/data/propers/common/vespers-capitulum-advent.yml @@ -0,0 +1,13 @@ +# Sourced from Divinum Officium's Monastic block ("[Adv Vespera]", +# Psalterium/Special/Major Special.txt). Not live-query-verified (see +# TODO.md) — read directly from source. +id: vespers-capitulum-advent +text: + la: "Non auferétur sceptrum de Juda, et dux de fémore ejus, donec véniat qui mitténdus est; et ipse erit exspectátio géntium." + en: "The sceptre shall not be taken away from Juda, nor a ruler from his thigh, till he comes that is to be sent, and he shall be the expectation of nations." +citation: + la: "Gen 49:10" + en: "Gen 49:10" +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-capitulum-lent.yml b/src/data/propers/common/vespers-capitulum-lent.yml new file mode 100644 index 0000000..7f7b20d --- /dev/null +++ b/src/data/propers/common/vespers-capitulum-lent.yml @@ -0,0 +1,13 @@ +# Sourced from Divinum Officium's Monastic block ("[Quad Vespera_]", +# Psalterium/Special/Major Special.txt). Not live-query-verified (see +# TODO.md) — read directly from source. +id: vespers-capitulum-lent +text: + la: "Inter vestíbulum et altáre plorábunt sacerdótes, minístri Dómini, et dicent: Parce, Dómine, parce pópulo tuo: et ne des hereditátem tuam in oppróbrium, ut dominéntur eis natiónes." + en: "Between the porch and the altar the priests the Lord's ministers shall weep, and shall say: Spare, O Lord, spare thy people: and give not thy inheritance to reproach, that the heathen should rule over them." +citation: + la: "Joel 2:17" + en: "Joel 2:17" +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-capitulum-paschaltide.yml b/src/data/propers/common/vespers-capitulum-paschaltide.yml new file mode 100644 index 0000000..f6ebdc0 --- /dev/null +++ b/src/data/propers/common/vespers-capitulum-paschaltide.yml @@ -0,0 +1,16 @@ +# Sourced from Divinum Officium's Monastic block: "[Pasch Vespera] = +# @:Pasch Laudes" — a bare alias, so Paschaltide Vespers shares its +# chapter with Paschaltide Lauds (see lauds-capitulum-paschaltide.yml). +# Covers the whole Easter-to-Pentecost span uniformly — no separate +# Ascensiontide/Pentecost-specific chapter exists in the source. Not +# live-query-verified (see TODO.md) — read directly from source. +id: vespers-capitulum-paschaltide +text: + la: "Christus resúrgens ex mórtuis jam non móritur, mors illi ultra non dominábitur. Quod enim mórtuus est peccáto, mórtuus est semel: quod autem vivit, vivit Deo." + en: "Knowing that Christ rising again from the dead, dieth now no more, death shall no more have dominion over him. For in that he died to sin, he died once; but in that he liveth, he liveth unto God." +citation: + la: "Rom 6:9-10" + en: "Rom 6:9-10" +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-capitulum-passiontide.yml b/src/data/propers/common/vespers-capitulum-passiontide.yml new file mode 100644 index 0000000..4c50715 --- /dev/null +++ b/src/data/propers/common/vespers-capitulum-passiontide.yml @@ -0,0 +1,13 @@ +# Sourced from Divinum Officium's Monastic block ("[Quad5 Vespera]", +# Psalterium/Special/Major Special.txt). Not live-query-verified (see +# TODO.md) — read directly from source. +id: vespers-capitulum-passiontide +text: + la: "Tu autem, Dómine Sábaoth, qui júdicas juste, et probas renes et corda, vídeam ultiónem tuam ex eis: tibi enim revelávi causam meam, Dómine, Deus meus." + en: "But thou, O Lord of Sabaoth, who judgest justly, and triest the reins and hearts, let me see thy revenge on them: for to thee I have revealed my cause, O Lord my God." +citation: + la: "Jer 11:20" + en: "Jer 11:20" +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-capitulum-saturday.yml b/src/data/propers/common/vespers-capitulum-saturday.yml new file mode 100644 index 0000000..1ce5af3 --- /dev/null +++ b/src/data/propers/common/vespers-capitulum-saturday.yml @@ -0,0 +1,18 @@ +# Sourced from Divinum Officium's Monastic block: "[Feria Vespera] +# (feria 7)" = "@Tempora/Pent01-0:Capitulum Laudes" — Saturday Vespers' +# own chapter is a cross-reference to the temporal-proper file for the +# first ordinary Sunday after Pentecost's own Lauds chapter (TemporaM/ +# Pent01-0.txt inherits this section unchanged from the base Tempora/ +# Pent01-0.txt, no Monastic-specific override). Not live-query-verified +# (see TODO.md) — read directly from source, same confidence level as the +# Vespers psalmody. +id: vespers-capitulum-saturday +text: + la: "O Altitúdo divitiárum sapiéntiæ, et sciéntiæ Dei, quam incomprehensibília sunt judícia ejus, et investigábiles viæ ejus!" + en: "O the depth of the riches of the wisdom and of the knowledge of God! How incomprehensible are his judgments, and how unsearchable his ways!" +citation: + la: "Rom 11:33" + en: "Rom 11:33" +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-capitulum-sunday-ferial.yml b/src/data/propers/common/vespers-capitulum-sunday-ferial.yml new file mode 100644 index 0000000..bd770a2 --- /dev/null +++ b/src/data/propers/common/vespers-capitulum-sunday-ferial.yml @@ -0,0 +1,17 @@ +# Sourced from Divinum Officium's Monastic block (Psalterium/Special/Major +# Special.txt): "[Dominica Vespera_]" gives the text directly; "[Feria +# Vespera] = @:Dominica Vespera" (Mon-Fri) is a bare alias to the same +# text, not a separate ferial chapter — so Sunday and the five weekdays +# genuinely share one chapter here. Not live-query-verified per weekday +# (see TODO.md) — read from the explicitly-labeled Monastic source block, +# same confidence level as the Vespers psalmody. +id: vespers-capitulum-sunday-ferial +text: + la: "Benedíctus Deus, et Pater Dómini nostri Jesu Christi, Pater misericordiárum, et Deus totíus consolatiónis, qui consolátur nos in omni tribulatióne nostra." + en: "Blessed be the God and Father of our Lord Jesus Christ, the Father of mercies, and the God of all comfort, who comforteth us in all our tribulation." +citation: + la: "2 Cor 1:3-4" + en: "2 Cor 1:3-4" +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-hymn-advent.yml b/src/data/propers/common/vespers-hymn-advent.yml new file mode 100644 index 0000000..1ac442d --- /dev/null +++ b/src/data/propers/common/vespers-hymn-advent.yml @@ -0,0 +1,78 @@ +# Sourced from Divinum Officium's Monastic block ("[HymnusM Adv Vespera]", +# Psalterium/Special/Major Special.txt) — full separate Monastic text +# ("Cónditor alme síderum", the older/more familiar form of this hymn), +# not a substitution against the Roman "Creátor alme síderum". English is +# the given translation of the *Roman* text ("Bright builder of the +# heavenly poles... Jesus, Redeemer of mankind"), which differs from the +# Monastic Latin in specific imagery (stanza 2 especially — "lest the +# world perish by the deceit of the demon" vs. "grieving that the world +# would perish by the ruin of death"), not just word order — kept anyway +# as the closest available English, `draft`. Not live-query-verified (see +# TODO.md). +id: vespers-hymn-advent +text: + la: | + Cónditor alme síderum, + Ætérna lux credéntium, + Christe, Redémptor ómnium, + Exáudi preces súpplicum. + + Qui cóndolens intéritu + Mortis períre sǽculum, + Salvásti mundum lánguidum, + Donans reis remédium, + + Vergénte mundi véspere, + Uti sponsus de thálamo, + Egréssus honestíssima + Vírginis matris cláusula. + + Cujus forti poténtiæ + Genu curvántur ómnia; + Cæléstia, terréstria + Nutu faténtur súbdita. + + Te deprecámur, hágie, + Ventúre judex sǽculi, + Consérva nos in témpore + Hostis a telo pérfidi. + + Laus, honor, virtus, glória, + Deo Patri, et Fílio, + Sancto simul Paráclito, + In sæculórum sǽcula. + Amen. + en: | + Bright builder of the heavenly poles, + Eternal light of faithful souls, + Jesus, Redeemer of mankind, + Our humble prayers vouchsafe to mind. + + Who, lest the fraud of hell's black king + Should all men to destruction bring, + Didst, by an act of generous love, + The fainting world's physician prove. + + Who, that thou mightst our ransom pay + And wash the stains of sin away, + Wouldst from a Virgin's womb proceed + And on the cross a victim bleed. + + Whose glorious power, whose saving name + No sooner any voice can frame, + But heaven and earth and hell agree + To honour them with trembling knee. + + Thee, Christ, who at the latter day + Shalt be our Judge, we humbly pray + Such arms of heavenly grace to send + As may thy Church from foes defend. + + Be glory given and honour done + To God the Father and the Son + And to the Holy Ghost on high, + From age to age eternally. + Amen. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-hymn-friday.yml b/src/data/propers/common/vespers-hymn-friday.yml new file mode 100644 index 0000000..4f828fb --- /dev/null +++ b/src/data/propers/common/vespers-hymn-friday.yml @@ -0,0 +1,63 @@ +# Sourced from Divinum Officium's Monastic block ("[HymnusM Day5 Vespera]", +# Psalterium/Special/Major Special.txt): six substitutions against the base +# "[Hymnus Day5 Vespera]" text (replacing the opening line entirely, +# "Hóminis superne Cónditor" -> "Plasmátor hóminis Deus", and rewording +# most of stanzas 2-3), applied by hand here — not live-query-verified +# (see TODO.md). English has no separate Monastic variant in the source. +id: vespers-hymn-friday +text: + la: | + Plasmátor hóminis Deus, + Qui cuncta solus órdinans, + Humum jubes prodúcere + Reptántis et feræ genus: + + Qui magna rerum córpora, + Dictu jubéntis vívida, + Ut sérviant per órdinem, + Subdens dedísti hómini: + + Repélle, a servis tuis, + Quidquid per immundítiam + Aut móribus se súggerit, + Aut áctibus se intérserit. + + Da gaudiórum prǽmia, + Da gratiárum múnera: + Dissólve litis víncula: + Astrínge pacis fœ́dera. + + Præsta, Pater piíssime, + Patríque compar Únice, + Cum Spíritu Paráclito + Regnans per omne sǽculum. + Amen. + en: | + Maker of man, who from thy throne + Dost order all things, God alone; + By whose decree the teeming earth + To reptile and to beast gave birth: + + The mighty forms that fill the land, + Instinct with life at thy command, + Are given subdued to humankind + For service in their rank assigned. + + From all thy servants drive away + Whate'er of thought impure to-day + Hath been with open action blent, + Or mingled with the heart's intent. + + In heaven thine endless joys bestow, + And grant thy gifts of grace below; + From chains of strife our souls release, + Bind fast the gentle bands of peace. + + Grant this, O Father, ever One + With Christ, thy sole-begotten Son, + Whom, with the Spirit we adore, + One God, both now and evermore. + Amen. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-hymn-lent.yml b/src/data/propers/common/vespers-hymn-lent.yml new file mode 100644 index 0000000..82475cf --- /dev/null +++ b/src/data/propers/common/vespers-hymn-lent.yml @@ -0,0 +1,63 @@ +# Sourced from Divinum Officium's Monastic block ("[HymnusM Quad +# Vespera]", Psalterium/Special/Major Special.txt): two substitutions +# against the base "[Hymnus Quad Vespera]" text — a word-order swap in +# stanza 3 ("Ad nóminis laudem tui" -> "Ad laudem tui nóminis") and a full +# replacement of stanza 4 — applied by hand here. Not live-query-verified +# (see TODO.md). English has no separate Monastic variant in the source. +id: vespers-hymn-lent +text: + la: | + Audi, benígne Cónditor, + Nostras preces cum flétibus, + In hoc sacro jejúnio + Fusas quadragenário. + + Scrutátor alme córdium, + Infírma tu scis vírium: + Ad te revérsis éxhibe + Remissiónis grátiam. + + Multum quidem peccávimus, + Sed parce confiténtibus: + Ad laudem tui nóminis + Confer medélam lánguidis. + + Sic corpus extra cónteri + Dona per abstinéntiam, + Jejúnet ut mens sóbria + A labe prorsus críminum. + + Præsta, beáta Trínitas, + Concéde, simplex Únitas; + Ut fructuósa sint tuis + Jejuniórum múnera. + Amen. + en: | + O kind Creator, bow thine ear + To mark the cry, to know the tear + Before thy throne of mercy spent + In this thy holy fast of Lent. + + Our hearts are open, Lord, to thee: + Thou knowest our infirmity; + Pour out on all who seek thy face + Abundance of thy pardoning grace. + + Our sins are many, this we know; + Spare us, good Lord, thy mercy show; + And for the honour of thy name + Our fainting souls to life reclaim. + + Give us the self-control that springs + From discipline of outward things, + That fasting inward secretly + The soul may purely dwell with thee. + + We pray thee, Holy Trinity, + One God, unchanging Unity, + That we from this our abstinence + May reap the fruits of penitence. + Amen. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-hymn-monday.yml b/src/data/propers/common/vespers-hymn-monday.yml new file mode 100644 index 0000000..90b8eac --- /dev/null +++ b/src/data/propers/common/vespers-hymn-monday.yml @@ -0,0 +1,62 @@ +# Sourced from Divinum Officium's Monastic block ("[HymnusM Day1 Vespera]", +# Psalterium/Special/Major Special.txt): two word substitutions against the +# base "[Hymnus Day1 Vespera]" text ("adáugeat" -> "invéniat"; "Hæc" -> +# "Ut", in the 4th stanza), applied by hand here — not live-query-verified +# (see TODO.md). English has no separate Monastic variant in the source. +id: vespers-hymn-monday +text: + la: | + Imménse cæli Cónditor, + Qui mixta ne confúnderent, + Aquæ fluénta dívidens, + Cælum dedísti límitem. + + Firmans locum cæléstibus, + Simúlque terræ rívulis; + Ut unda flammas témperet, + Terræ solum ne díssipent. + + Infúnde nunc, piíssime, + Donum perénnis grátiæ: + Fraudis novæ ne cásibus + Nos error átterat vetus. + + Lucem fides invéniat: + Sic lúminis jubar ferat: + Ut vana cuncta próterat: + Hanc falsa nulla cómprimant. + + Præsta, Pater piíssime, + Patríque compar Únice, + Cum Spíritu Paráclito + Regnans per omne sǽculum. + Amen. + en: | + O great Creator of the sky, + Who wouldest not the floods on high + With earthly waters to confound, + But mad'st the firmament their bound; + + The floods above thou didst ordain; + The floods below thou didst restrain: + That moisture might attemper heat, + Lest the parched earth should ruin meet. + + Upon our souls, good Lord, bestow + Thy gift of grace in endless flow: + Lest some renewed deceit or wile + Of former sin should us beguile. + + Let faith discover heav'nly light; + So shall its rays direct us right: + And let this faith each error chase, + And never give to falsehood place. + + Grant this, O Father, ever One + With Christ, thy sole-begotten Son, + And Holy Ghost, whom all adore, + Reigning and blest forevermore. + Amen. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-hymn-paschaltide.yml b/src/data/propers/common/vespers-hymn-paschaltide.yml new file mode 100644 index 0000000..035bdb8 --- /dev/null +++ b/src/data/propers/common/vespers-hymn-paschaltide.yml @@ -0,0 +1,96 @@ +# Sourced from Divinum Officium's Monastic block ("[HymnusM Pasch +# Vespera]", Psalterium/Special/Major Special.txt) — "Ad cenam Agni +# próvidi", the older Monastic form, full separate text (not a +# substitution against the Roman Urban VIII revision, "Ad régias Agni +# dapes", which differs substantially stanza by stanza, not just in +# wording). English is the given translation of that Roman revision — +# conventionally paired with this older Latin in many hymnals despite the +# textual differences, but a real mismatch worth flagging, same caution +# as the Sunday Vespers hymn. Not live-query-verified (see TODO.md). +id: vespers-hymn-paschaltide +text: + la: | + Ad cenam Agni próvidi, + Et stolis albis cándidi, + Post tránsitum maris Rubri, + Christo canámus Príncipi. + + Cujus corpus sanctíssimum, + In ara crucis tórridum, + Cruóre ejus róseo + Gustándo vívimus Deo. + + Protécti Paschæ véspere + A devastánte Angelo, + Erépti de duríssimo + Pharaónis império. + + Jam Pascha nostrum Christus est, + Qui immolátus agnus est: + Sinceritátis ázyma, + Caro ejus obláta est. + + O vere digna hóstia, + Per quam fracta sunt tártara, + Redémpta plebs captiváta, + Réddita vitæ prǽmia. + + Consúrgit Christus túmulo, + Victor redit de bárathro, + Tyránnum trudens vínculo, + Et Paradísum réserans. + + Quǽsumus, Auctor ómnium, + In hoc pascháli gáudio, + Ab omni mortis ímpetu + Tuum defénde pópulum. + + Glória tibi Dómine, + Qui surrexísti a mórtuis, + Cum Patre et Sancto Spíritu, + In sempitérna sǽcula. + Amen. + en: | + At the Lamb's high feast we sing + Praise to our victorious King, + Who hath washed us in the tide + Flowing from his pierced side. + + Praise we him whose love divine + Gives the guests his blood for wine, + Gives his body for the feast, + Love the victim, love the priest. + + Where the paschal blood is poured, + Death's dark angel sheathes his sword; + Israel's hosts triumphant go + Through the wave that drowns the foe. + + Christ, the Lamb whose blood was shed, + Paschal victim, paschal bread; + With sincerity and love + Eat we manna from above. + + Mighty victim from the sky, + Powers of hell beneath thee lie; + Death is conquered in the fight; + Thou hast brought us life and light. + + Now thy banner thou dost wave; + Vanquished Satan and the grave; + Angels join his praise to tell — + See o'erthrown the prince of hell. + + Paschal triumph, paschal joy, + Only sin can this destroy; + From the death of sin set free, + Souls re-born, dear Lord, in Thee. + + Hymns of glory, songs of praise, + Father, unto thee we raise; + Risen Lord, all praise to thee, + Ever with the Spirit be. + Amen. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-hymn-passiontide.yml b/src/data/propers/common/vespers-hymn-passiontide.yml new file mode 100644 index 0000000..d55fc98 --- /dev/null +++ b/src/data/propers/common/vespers-hymn-passiontide.yml @@ -0,0 +1,93 @@ +# Sourced from Divinum Officium's Monastic block ("[HymnusM Quad5 +# Vespera]", Psalterium/Special/Major Special.txt) — "Vexílla Regis +# pródeunt", full separate Monastic text (not a substitution against the +# Roman version, which differs meaningfully in several stanzas — caught +# and fixed a first-pass transcription slip here where the second-to-last +# stanza's closing lines had been copied from the Roman text instead of +# the Monastic one actually being transcribed). The source marks that +# stanza's third line with a seasonal branch — "Hoc Passiónis témpore" +# (used here, for Passiontide proper) vs. "In hoc Pascháli gáudio" +# (Paschaltide, when this same hymn is reused for a Holy Cross feast +# falling then — not this app's concern). Also: real Passiontide practice +# omits the Gloria Patri from the last stanza — kept that way here, +# matching the source exactly (see vespers-responsory-passiontide.yml's +# own note). Not live-query-verified (see TODO.md). +id: vespers-hymn-passiontide +text: + la: | + Vexílla Regis pródeunt: + Fulget Crucis mystérium, + Quo carne carnis Cónditor + Suspénsus est patíbulo. + + Quo vulnerátus ínsuper + Mucróne diro lánceæ, + Ut nos laváret crímine, + Manávit unda et sánguine. + + Impléta sunt quæ cóncinit + David fidéli cármine, + Dicens: In natiónibus + Regnávit a ligno Deus. + + Arbor decóra et fúlgida, + Ornáta Regis púrpura, + Elécta digno stípite + Tam sancta membra tángere. + + Beáta, cujus bráchiis + Sæcli pepéndit prétium, + Statéra facta córporis, + Prædámque tulit tártari. + + O Crux, ave, spes única, + In hac triúmphi glória + Hoc Passiónis témpore + Auge piis justítiam, + Reísque dona véniam. + + Te summa, Deus, Trínitas, + Colláudet omnis spíritus: + Quos per Crucis mystérium + Salvas, rege per sǽcula. + Amen. + en: | + Abroad the regal banners fly, + Now shines the cross's mystery; + Upon it Life did death endure, + And yet by death did life procure. + + Who, wounded with a direful spear, + Did, purposely to wash us clear + From stain of sin, pour out a flood + Of precious water mixed with blood. + + That which the prophet-king of old + Hath in mysterious verse foretold, + Is now accomplished, whilst we see + God ruling nations from a tree. + + O lovely and refulgent tree, + Adorned with purpled majesty; + Culled from a worthy stock, to bear + Those limbs which sanctified were. + + Blest tree, whose happy branches bore + The wealth that did the world restore; + The beam that did that body weigh + Which raised up hell's expected prey. + + O Cross, our one reliance, hail! + On this triumphant day avail, + This holy Passiontide, + To give fresh merit to the saint, + And pardon to the penitent. + + Blest Trinity, salvation's spring, + May every soul thy praises sing; + To those thou grantest conquest by + The holy cross, rewards apply. + Amen. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-hymn-saturday.yml b/src/data/propers/common/vespers-hymn-saturday.yml new file mode 100644 index 0000000..31de7c6 --- /dev/null +++ b/src/data/propers/common/vespers-hymn-saturday.yml @@ -0,0 +1,43 @@ +# Sourced from Divinum Officium's Monastic block ("[HymnusM Vespera]" in +# Latin/TemporaM/Pent01-0.txt, referenced from "[HymnusM Day6 Vespera]" = +# "@Tempora/Pent01-0:HymnusM Vespera" in Psalterium/Special/Major +# Special.txt) — full separate Monastic text this time ("O lux beáta +# Trínitas"), not a substitution against a base hymn. Not +# live-query-verified (see TODO.md) — read directly from source. +id: vespers-hymn-saturday +text: + la: | + O lux beáta Trínitas, + Et principális Únitas, + Jam sol recédit ígneus, + Infúnde lumen córdibus. + + Te mane laudum cármine, + Te deprecémur véspere: + Te nostra supplex glória + Per cuncta laudet sǽcula. + + Deo Patri sit glória, + Ejúsque soli Fílio, + Cum Spíritu Paráclito, + Et nunc, et in perpétuum. + Amen. + en: | + O Trinity of blessed Light, + O Unity of sovereign might, + as now the fiery sun departs, + shed Thou Thy beams within our hearts. + + To Thee our morning song of praise, + to Thee our evening prayer we raise; + Thee may our glory evermore + in lowly reverence adore. + + All laud to God the Father be; + all praise, Eternal Son, to Thee; + all glory, as is ever meet, + to God the Holy Paraclete. + Amen. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-hymn-sunday.yml b/src/data/propers/common/vespers-hymn-sunday.yml new file mode 100644 index 0000000..5add5fd --- /dev/null +++ b/src/data/propers/common/vespers-hymn-sunday.yml @@ -0,0 +1,65 @@ +# Sourced from Divinum Officium's Monastic block ("[HymnusM Day0 Vespera]", +# Psalterium/Special/Major Special.txt). The Monastic Latin is stored there +# as two word-order/wording substitutions against the base "[Hymnus Day0 +# Vespera]" text ("Illábitur tetrum chaos" -> "Tetrum chaos illábitur"; +# "Cæléste pulset óstium:" -> "Cælórum pulset íntimum,"), applied by hand +# here rather than left as a diff — not live-query-verified (see TODO.md). +# English has no separate Monastic variant in the source (the wording +# differences don't affect translation), so it's the same text either way. +id: vespers-hymn-sunday +text: + la: | + Lucis Creátor óptime, + Lucem diérum próferens, + Primórdiis lucis novæ, + Mundi parans oríginem: + + Qui mane junctum vésperi + Diem vocári prǽcipis: + Tetrum chaos illábitur, + Audi preces cum flétibus. + + Ne mens graváta crímine, + Vitæ sit exsul múnere, + Dum nil perénne cógitat, + Seséque culpis ílligat. + + Cælórum pulset íntimum, + Vitále tollat prǽmium: + Vitémus omne nóxium: + Purgémus omne péssimum. + + Præsta, Pater piíssime, + Patríque compar Únice, + Cum Spíritu Paráclito + Regnans per omne sǽculum. + Amen. + en: | + O blest Creator of the light, + Who mak'st the day with radiance bright, + And o'er the forming world didst call + The light from chaos first of all; + + Whose wisdom joined in meet array + The morn and eve, and named them day: + Night comes with all its darkling fears; + Regard thy people's prayers and tears. + + Lest, sunk in sin, and whelmed with strife, + They lose the gift of endless life; + While thinking but the thoughts of time, + They weave new chains of woe and crime. + + But grant them grace that they may strain + The heavenly gate and prize to gain: + Each harmful lure aside to cast, + And purge away each error past. + + O Father, that we ask be done, + Through Jesus Christ, thine only Son; + Who, with the Holy Ghost and thee, + Doth live and reign eternally. + Amen. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-hymn-thursday.yml b/src/data/propers/common/vespers-hymn-thursday.yml new file mode 100644 index 0000000..4f6107e --- /dev/null +++ b/src/data/propers/common/vespers-hymn-thursday.yml @@ -0,0 +1,64 @@ +# Sourced from Divinum Officium's Monastic block ("[HymnusM Day4 Vespera]", +# Psalterium/Special/Major Special.txt): five substitutions against the +# base "[Hymnus Day4 Vespera]" text ("fértili natos aqua" -> "ex aquis +# ortum genus"; "relínquis" -> "remíttis"; "érigens" -> "írrigans"; +# "répleant" -> "rápiant"; "éfferat" -> "levet"), applied by hand here — +# not live-query-verified (see TODO.md). English has no separate Monastic +# variant in the source. +id: vespers-hymn-thursday +text: + la: | + Magnæ Deus poténtiæ, + Qui ex aquis ortum genus + Partim remíttis gúrgiti, + Partim levas in áëra. + + Demérsa lymphis ímprimens, + Subvécta cælis írrigans: + Ut stirpe ab una pródita, + Divérsa rápiant loca: + + Largíre cunctis sérvulis, + Quos mundat unda Sánguinis, + Nescíre lapsus críminum, + Nec ferre mortis tǽdium. + + Ut culpa nullum déprimat: + Nullum levet jactántia: + Elísa mens ne cóncidat: + Eláta mens ne córruat. + + Præsta, Pater piíssime, + Patríque compar Únice, + Cum Spíritu Paráclito + Regnans per omne sǽculum. + Amen. + en: | + O sovereign Lord of nature's might, + Who bad'st the water's birth divide; + Part in the heavens to take their flight, + And part in ocean's deep to hide; + + These low obscured, on airy wing + Exalted those, that either race, + Though from one element they spring, + Might serve thee in a different place. + + Grant, Lord, that we thy servants all, + Saved by thy tide of cleansing blood, + No more 'neath sin's dominion fall, + Nor fear the thought of death's dark flood! + + Thy varied love each spirit bless, + The humble cheer, the high control; + Check in each heart its proud excess, + But raise the meek and contrite soul! + + This boon, O Father, we entreat, + This blessing grant, Eternal Son, + And Holy Ghost, the Paraclete, + Both now, and while the ages run. + Amen. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-hymn-tuesday.yml b/src/data/propers/common/vespers-hymn-tuesday.yml new file mode 100644 index 0000000..22f24c4 --- /dev/null +++ b/src/data/propers/common/vespers-hymn-tuesday.yml @@ -0,0 +1,64 @@ +# Sourced from Divinum Officium's Monastic block ("[HymnusM Day2 Vespera]", +# Psalterium/Special/Major Special.txt): four word substitutions against +# the base "[Hymnus Day2 Vespera]" text ("alme" -> "ingens"; "séparans" -> +# "éruens"; "dedísti" -> "dedíst[i]" — the source's own bracket notation, +# kept as-is; "ictum" -> "actum"), applied by hand here — not +# live-query-verified (see TODO.md). English has no separate Monastic +# variant in the source. +id: vespers-hymn-tuesday +text: + la: | + Tellúris ingens Cónditor, + Mundi solum qui éruens, + Pulsis aquæ moléstiis, + Terram dedíst[i] immóbilem: + + Ut germen aptum próferens, + Fulvis decóra flóribus, + Fecúnda fructu sísteret, + Pastúmque gratum rédderet. + + Mentis perústæ vúlnera + Munda viróre grátiæ: + Ut facta fletu díluat, + Motúsque pravos átterat. + + Jussis tuis obtémperet: + Nullis malis appróximet: + Bonis repléri gáudeat, + Et mortis actum nésciat. + + Præsta, Pater piíssime, + Patríque compar Únice, + Cum Spíritu Paráclito + Regnans per omne sǽculum. + Amen. + en: | + Earth's mighty Maker, whose command + Raised from the sea the solid land; + And drove each billowy heap away, + And bade the earth stand firm for aye: + + That so, with flowers of golden hue, + The seeds of each it might renew; + And fruit-trees bearing fruit might yield, + And pleasant pasture of the field: + + Our spirit's rankling wounds efface + With dewy freshness of thy grace: + That grief may cleanse each deed of ill, + And o'er each lust may triumph still. + + Let every soul thy law obey, + And keep from every evil way; + Rejoice each promised good to win, + And flee from every mortal sin. + + Hear thou our prayer, Almighty King! + Hear thou our praises, while we sing, + Adoring with the heavenly host, + The Father, Son, and Holy Ghost! + Amen. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-hymn-wednesday.yml b/src/data/propers/common/vespers-hymn-wednesday.yml new file mode 100644 index 0000000..ccf342f --- /dev/null +++ b/src/data/propers/common/vespers-hymn-wednesday.yml @@ -0,0 +1,64 @@ +# Sourced from Divinum Officium's Monastic block ("[HymnusM Day3 Vespera]", +# Psalterium/Special/Major Special.txt): three substitutions against the +# base "[Hymnus Day3 Vespera]" text ("mundi plagas" -> "centrum poli"; +# "Dum solis accéndis rotam" -> "Solis rotam constítuens"; "Expélle noctem +# córdium:" -> "Illúmina cor hóminum,"), applied by hand here — not +# live-query-verified (see TODO.md). English has no separate Monastic +# variant in the source. +id: vespers-hymn-wednesday +text: + la: | + Cæli Deus sanctíssime, + Qui lúcidas centrum poli + Candóre pingis ígneo, + Augens decóro lúmine: + + Quarto die qui flámmeam + Solis rotam constítuens, + Lunæ minístras órdinem, + Vagósque cursus síderum: + + Ut nóctibus, vel lúmini + Diremptiónis términum, + Primórdiis et ménsium + Signum dares notíssimum; + + Illúmina cor hóminum, + Abstérge sordes méntium: + Resólve culpæ vínculum: + Evérte moles críminum. + + Præsta, Pater piíssime, + Patríque compar Únice, + Cum Spíritu Paráclito + Regnans per omne sǽculum. + Amen. + en: | + O God, whose hand hath spread the sky, + And all its shining hosts on high, + And painting it with fiery light, + Made it so beauteous and so bright: + + Thou, when the fourth day was begun, + Didst frame the circle of the sun, + And set the moon for ordered change, + And planets for their wider range: + + To night and day, by certain line, + Their varying bounds thou didst assign; + And gav'st a signal, known and meet, + For months begun and months complete. + + Enlighten thou the hearts of men: + Polluted souls make pure again: + Unloose the bands of guilt within: + Remove the burden of our sin. + + Grant this, O Father, ever One + With Christ thy sole-begotten Son, + Whom, with the Spirit we adore, + One God, both now and evermore. + Amen. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-responsory-advent.yml b/src/data/propers/common/vespers-responsory-advent.yml new file mode 100644 index 0000000..ad63d66 --- /dev/null +++ b/src/data/propers/common/vespers-responsory-advent.yml @@ -0,0 +1,23 @@ +# Sourced from Divinum Officium's Monastic block ("[Responsory Adv +# Vespera_]", the non-Cistercian branch "[Responsory Adv Vespera]" +# resolves to — Psalterium/Special/Major Special.txt). Not +# live-query-verified (see TODO.md) — read directly from source. +id: vespers-responsory-advent +text: + la: | + R.br. Osténde nobis Dómine * Misericórdiam tuam. + R. Osténde nobis Dómine * Misericórdiam tuam. + V. Et salutáre tuum da nobis. + R. Misericórdiam tuam. + V. Glória Patri, et Fílio, * et Spirítui Sancto. + R. Osténde nobis Dómine * Misericórdiam tuam. + en: | + R.br. Show us, O Lord, * thy mercy. + R. Show us, O Lord, * thy mercy. + V. And grant us thy salvation. + R. Thy mercy. + V. Glory be to the Father, and to the Son, * and to the Holy Ghost. + R. Show us, O Lord, * thy mercy. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-responsory-ferial.yml b/src/data/propers/common/vespers-responsory-ferial.yml new file mode 100644 index 0000000..b6e5fd1 --- /dev/null +++ b/src/data/propers/common/vespers-responsory-ferial.yml @@ -0,0 +1,23 @@ +# Sourced from Divinum Officium's Monastic block ("[Responsory Feria +# Vespera_]", the non-Cistercian branch "[Responsory Feria Vespera]" +# resolves to — Psalterium/Special/Major Special.txt). Shared Mon-Fri. +# Not live-query-verified (see TODO.md) — read directly from source. +id: vespers-responsory-ferial +text: + la: | + R.br. Benedícam Dóminum * In omni témpore. + R. Benedícam Dóminum * In omni témpore. + V. Semper laus ejus in ore meo. + R. In omni témpore. + V. Glória Patri, et Fílio, * et Spirítui Sancto. + R. Benedícam Dóminum * In omni témpore. + en: | + R.br. I will bless the Lord * at all times. + R. I will bless the Lord * at all times. + V. His praise shall be always in my mouth. + R. At all times. + V. Glory be to the Father, and to the Son, * and to the Holy Ghost. + R. I will bless the Lord * at all times. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-responsory-lent.yml b/src/data/propers/common/vespers-responsory-lent.yml new file mode 100644 index 0000000..e0bcf6d --- /dev/null +++ b/src/data/propers/common/vespers-responsory-lent.yml @@ -0,0 +1,22 @@ +# Sourced from Divinum Officium's Monastic block ("[Responsory Quad +# Vespera]", Psalterium/Special/Major Special.txt). Not +# live-query-verified (see TODO.md) — read directly from source. +id: vespers-responsory-lent +text: + la: | + R.br. Scápulis suis * obumbrábit tibi. + R. Scápulis suis * obumbrábit tibi. + V. Et sub pennis ejus sperábis. + R. Obumbrábit tibi. + V. Glória Patri, et Fílio, * et Spirítui Sancto. + R. Scápulis suis * obumbrábit tibi. + en: | + R.br. He will overshadow * thee with his shoulders. + R. He will overshadow * thee with his shoulders. + V. And under his wings thou shalt trust. + R. Thee with his shoulders. + V. Glory be to the Father, and to the Son, * and to the Holy Ghost. + R. He will overshadow * thee with his shoulders. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-responsory-paschaltide.yml b/src/data/propers/common/vespers-responsory-paschaltide.yml new file mode 100644 index 0000000..1c834c6 --- /dev/null +++ b/src/data/propers/common/vespers-responsory-paschaltide.yml @@ -0,0 +1,25 @@ +# Sourced from Divinum Officium's Monastic block: "[Responsory Pasch +# Vespera]" resolves (non-Cistercian branch) to "@Psalterium/Special/ +# Minor Special:Responsory breve Pasch Sexta" (Psalterium/Special/Minor +# Special.txt) — distinct from Lauds' own Paschaltide responsory (see +# lauds-responsory-paschaltide.yml). Not live-query-verified (see +# TODO.md) — read directly from source. +id: vespers-responsory-paschaltide +text: + la: | + R.br. Surréxit Dóminus vere, * Allelúja, allelúja. + R. Surréxit Dóminus vere, * Allelúja, allelúja. + V. Et appáruit Simóni. + R. Allelúja, allelúja. + V. Glória Patri, et Fílio, * et Spirítui Sancto. + R. Surréxit Dóminus vere, * Allelúja, allelúja. + en: | + R.br. The Lord is risen indeed, * alleluia, alleluia. + R. The Lord is risen indeed, * alleluia, alleluia. + V. And hath appeared to Simon. + R. Alleluia, alleluia. + V. Glory be to the Father, and to the Son, * and to the Holy Ghost. + R. The Lord is risen indeed, * alleluia, alleluia. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-responsory-passiontide.yml b/src/data/propers/common/vespers-responsory-passiontide.yml new file mode 100644 index 0000000..c40b232 --- /dev/null +++ b/src/data/propers/common/vespers-responsory-passiontide.yml @@ -0,0 +1,21 @@ +# Sourced from Divinum Officium's Monastic block ("[Responsory Quad5 +# Vespera_]", the non-Cistercian branch "[Responsory Quad5 Vespera]" +# resolves to — Psalterium/Special/Major Special.txt). Not +# live-query-verified (see TODO.md) — read directly from source. +id: vespers-responsory-passiontide +text: + la: | + R.br. De ore leónis * líbera me, Dómine. + R. De ore leónis * líbera me, Dómine. + V. Et a córnibus unicórnium humilitátem meam. + R. Líbera me, Dómine. + R. De ore leónis * líbera me, Dómine. + en: | + R.br. From the lion's mouth, * O Lord, save me. + R. From the lion's mouth, * O Lord, save me. + V. And my lowness from the horns of the unicorns. + R. O Lord, save me. + R. From the lion's mouth, * O Lord, save me. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-responsory-saturday.yml b/src/data/propers/common/vespers-responsory-saturday.yml new file mode 100644 index 0000000..ff09530 --- /dev/null +++ b/src/data/propers/common/vespers-responsory-saturday.yml @@ -0,0 +1,22 @@ +# Sourced from Divinum Officium's Monastic block ("[Responsory Feria +# Vespera] (feria 7)", Psalterium/Special/Major Special.txt). Not +# live-query-verified (see TODO.md) — read directly from source. +id: vespers-responsory-saturday +text: + la: | + R.br. Magnus Dóminus noster, * et magna virtus ejus. + R. Magnus Dóminus noster, * et magna virtus ejus. + V. Et sapiéntiæ ejus non est númerus. + R. Et magna virtus ejus. + V. Glória Patri, et Fílio, * et Spirítui Sancto. + R. Magnus Dóminus noster, * et magna virtus ejus. + en: | + R.br. Great is our Lord, * and great is his power. + R. Great is our Lord, * and great is his power. + V. And of his wisdom there is no number. + R. And great is his power. + V. Glory be to the Father, and to the Son, * and to the Holy Ghost. + R. Great is our Lord, * and great is his power. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-responsory-sunday.yml b/src/data/propers/common/vespers-responsory-sunday.yml new file mode 100644 index 0000000..684cf1d --- /dev/null +++ b/src/data/propers/common/vespers-responsory-sunday.yml @@ -0,0 +1,22 @@ +# Sourced from Divinum Officium's Monastic block ("[Responsory Dominica +# Vespera]", Psalterium/Special/Major Special.txt). Not live-query-verified +# (see TODO.md) — read directly from source. +id: vespers-responsory-sunday +text: + la: | + R.br. Quam magnificáta sunt * ópera tua, Dómine. + R. Quam magnificáta sunt * ópera tua, Dómine. + V. Ómnia in sapiéntia fecísti. + R. Ópera tua Dómine. + V. Glória Patri, et Fílio, * et Spirítui Sancto. + R. Quam magnificáta sunt * ópera tua, Dómine. + en: | + R.br. How great are thy works, * O Lord. + R. How great are thy works, * O Lord. + V. Thou hast made all things in wisdom. + R. Thy works, O Lord. + V. Glory be to the Father, and to the Son, * and to the Holy Ghost. + R. How great are thy works, * O Lord. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-versicle-advent.yml b/src/data/propers/common/vespers-versicle-advent.yml new file mode 100644 index 0000000..14f66a5 --- /dev/null +++ b/src/data/propers/common/vespers-versicle-advent.yml @@ -0,0 +1,15 @@ +# The versicle right before the Magnificat, in Advent — the famous +# "Rorate cæli". Sourced from Divinum Officium's Monastic block ("[Adv +# Versum 3]", Psalterium/Special/Major Special.txt). Not +# live-query-verified (see TODO.md) — read directly from source. +id: vespers-versicle-advent +text: + la: | + V. Roráte, cæli, désuper, et nubes pluant justum. + R. Aperiátur terra, et gérminet Salvatórem. + en: | + V. Drop down dew, ye heavens, from above, and let the clouds rain the Just. + R. Let the earth be opened, and bud forth the Saviour. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-versicle-lent.yml b/src/data/propers/common/vespers-versicle-lent.yml new file mode 100644 index 0000000..17afccb --- /dev/null +++ b/src/data/propers/common/vespers-versicle-lent.yml @@ -0,0 +1,17 @@ +# The versicle right before the Magnificat, in Lent. Sourced from Divinum +# Officium's Monastic block ("[Quad Versum 3]", Psalterium/Special/Major +# Special.txt) — "[Quad Versum 2]" (Lauds' own) is a bare alias to this +# same text, so Lauds and Vespers share it here too (see +# lauds-versicle-lent.yml). Not live-query-verified (see TODO.md) — read +# directly from source. +id: vespers-versicle-lent +text: + la: | + V. Ángelis suis Deus mandávit de te. + R. Ut custódiant te in ómnibus viis tuis. + en: | + V. God hath given His Angels charge over thee. + R. To keep thee in all thy ways. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-versicle-paschaltide.yml b/src/data/propers/common/vespers-versicle-paschaltide.yml new file mode 100644 index 0000000..175f010 --- /dev/null +++ b/src/data/propers/common/vespers-versicle-paschaltide.yml @@ -0,0 +1,16 @@ +# The versicle right before the Magnificat, in Paschaltide. Sourced from +# Divinum Officium's Monastic block ("[Pasch Versum 3]", Psalterium/ +# Special/Major Special.txt). Distinct from Lauds' own Paschaltide +# versicle (see lauds-versicle-paschaltide.yml). Not live-query-verified +# (see TODO.md) — read directly from source. +id: vespers-versicle-paschaltide +text: + la: | + V. Mane nobíscum, Dómine, allelúja. + R. Quóniam advesperáscit, allelúja. + en: | + V. Stay with us, O Lord, alleluia. + R. Because it is towards evening, alleluia. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-versicle-passiontide.yml b/src/data/propers/common/vespers-versicle-passiontide.yml new file mode 100644 index 0000000..2ac423d --- /dev/null +++ b/src/data/propers/common/vespers-versicle-passiontide.yml @@ -0,0 +1,16 @@ +# The versicle right before the Magnificat, in Passiontide. Sourced from +# Divinum Officium's Monastic block ("[Quad5 Versum 3_]", the +# non-Cistercian branch "[Quad5 Versum 3]" resolves to — Psalterium/ +# Special/Major Special.txt). Not live-query-verified (see TODO.md) — +# read directly from source. +id: vespers-versicle-passiontide +text: + la: | + V. Éripe me, Dómine, ab hómine malo. + R. A viro iníquo éripe me. + en: | + V. Deliver me, O Lord, from the evil man. + R. Rescue me from the unjust man. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-versicle-saturday.yml b/src/data/propers/common/vespers-versicle-saturday.yml new file mode 100644 index 0000000..5b3c42b --- /dev/null +++ b/src/data/propers/common/vespers-versicle-saturday.yml @@ -0,0 +1,16 @@ +# The versicle right before the Magnificat. Sourced from Divinum +# Officium's Monastic block ("[Feria Versum 3] (feria 7)", Psalterium/ +# Special/Major Special.txt) — Saturday has its own, distinct from Sunday/ +# the weekdays. Not live-query-verified (see TODO.md) — read directly from +# source. +id: vespers-versicle-saturday +text: + la: | + V. Vespertína orátio ascéndat ad te, Dómine. + R. Et descéndat super nos misericórdia tua. + en: | + V. Let the evening prayer ascend unto thee, O Lord. + R. And let there descend upon us thy mercy. +status: + la: draft + en: draft diff --git a/src/data/propers/common/vespers-versicle-sunday-ferial.yml b/src/data/propers/common/vespers-versicle-sunday-ferial.yml new file mode 100644 index 0000000..bc860b7 --- /dev/null +++ b/src/data/propers/common/vespers-versicle-sunday-ferial.yml @@ -0,0 +1,16 @@ +# The versicle right before the Magnificat. Sourced from Divinum +# Officium's Monastic block: "[Dominica Versum 3]" gives the text +# directly; "[Feria Versum 3] = @:Dominica Versum 3" (Mon-Fri) is a bare +# alias to the same text — so Sunday and the five weekdays share this one. +# Not live-query-verified (see TODO.md) — read directly from source. +id: vespers-versicle-sunday-ferial +text: + la: | + V. Dirigátur, Dómine, orátio mea. + R. Sicut incénsum in conspéctu tuo. + en: | + V. O Lord, direct my prayer. + R. As incense in thy sight. +status: + la: draft + en: draft diff --git a/src/hours/compline.ts b/src/hours/compline.ts index 82f5257..890eebb 100644 --- a/src/hours/compline.ts +++ b/src/hours/compline.ts @@ -91,6 +91,9 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] { case 'suffrages': case 'lauds-preces': case 'day-collects': + case 'vespers-psalmody': + case 'vespers-office': + case 'magnificat': throw new Error(`Compline's ordo doesn't support a '${part.kind}' part`); } } diff --git a/src/hours/lauds.ts b/src/hours/lauds.ts index 25b945f..fffd1da 100644 --- a/src/hours/lauds.ts +++ b/src/hours/lauds.ts @@ -344,6 +344,9 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] { case 'variable': case 'day-collect': case 'nunc-dimittis': + case 'vespers-psalmody': + case 'vespers-office': + case 'magnificat': throw new Error(`Lauds' ordo doesn't support a '${part.kind}' part`); } } diff --git a/src/hours/none.ts b/src/hours/none.ts index eab41b7..6054fa7 100644 --- a/src/hours/none.ts +++ b/src/hours/none.ts @@ -67,6 +67,9 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] { case 'suffrages': case 'lauds-preces': case 'day-collects': + case 'vespers-psalmody': + case 'vespers-office': + case 'magnificat': throw new Error(`None's ordo doesn't support a '${part.kind}' part`); } } diff --git a/src/hours/prime.ts b/src/hours/prime.ts index ceed298..03bf22b 100644 --- a/src/hours/prime.ts +++ b/src/hours/prime.ts @@ -123,6 +123,9 @@ function resolvePart(part: HourPart, date: string, day: LiturgicalDay): Resolved case 'suffrages': case 'lauds-preces': case 'day-collects': + case 'vespers-psalmody': + case 'vespers-office': + case 'magnificat': throw new Error(`Prime's ordo doesn't support a '${part.kind}' part`); } } diff --git a/src/hours/resolve-common.ts b/src/hours/resolve-common.ts index 17650c2..16b748d 100644 --- a/src/hours/resolve-common.ts +++ b/src/hours/resolve-common.ts @@ -1,10 +1,23 @@ import type { ResolvedPart, ResolvedText } from './types'; -import type { Commemoration, DayWinner, LiturgicalDay } from '../calendar/types'; +import type { Commemoration, DayWinner, LiturgicalDay, Weekday } from '../calendar/types'; import type { ProperText } from '../propers'; import { getCommonProper, getTemporalProper } from '../propers'; import { getSaintRecord } from '../calendar/feasts'; import { resolveActiveOctave } from '../calendar'; import { splitAntiphon } from './antiphon'; +import vespersMagnificatAntiphonsData from '../data/hours/vespers-magnificat-antiphons.yml'; + +type BilingualText = Partial>; +type TranslationStatus = 'verified' | 'draft' | 'missing'; +interface MagnificatWeekdayDefault { + antiphon: BilingualText; + status?: Partial>; +} +// Monday-Saturday only — Sunday has no fixed weekday default (its own +// antiphon varies by Proper of the Time; see the data file's own header). +const vespersMagnificatAntiphons = vespersMagnificatAntiphonsData as Partial< + Record +>; function toResolvedText(proper: ProperText): ResolvedText { return { text: proper.text, status: proper.status, citation: proper.citation }; @@ -163,6 +176,25 @@ export function getMinorHourOverrideId(day: LiturgicalDay): string | undefined { */ const PASCHALTIDE_SEASONS = new Set(['eastertide', 'ascensiontide', 'pentecost']); +/** + * Which seasonal office-content suffix (if any) a day's season maps to — + * shared by Lauds' and Vespers' own resolveOffice, both of which fall + * back to the plain weekday default when this is undefined (and both + * check a per-feast/octave override first, ahead of this). Paschaltide + * covers eastertide/ascensiontide/pentecost uniformly, matching the + * source's own single "Pasch" block for both hours — no separate + * Ascension/Pentecost-specific chapter/hymn exists there. + */ +export function seasonalOfficeSuffix(season: string): string | undefined { + if (season === 'advent' || season === 'lent' || season === 'passiontide') { + return season; + } + if (PASCHALTIDE_SEASONS.has(season)) { + return 'paschaltide'; + } + return undefined; +} + /** * The reference engine's mechanical Paschaltide fallback for a Common * antiphon with no dedicated wholesale-different text of its own: a @@ -449,6 +481,48 @@ export function getBenedictusAntiphon(day: LiturgicalDay): ResolvedText { return { text: {}, status: { la: 'missing', en: 'missing' } }; } +/** + * Vespers' Magnificat antiphon — mirrors getBenedictusAntiphon's lookup + * order exactly (temporal id, then a saint's own `${propers}-magnificat- + * antiphon`, then their Common category's `magnificat-antiphon-${common}` + * — none of that content authored yet, a separate future pass the same + * shape as the Benedictus one was), but with one real difference: a bare + * ferial weekday (Mon-Sat) has actual content to fall back to — + * data/hours/vespers-magnificat-antiphons.yml's classic "quote the + * Magnificat's own text in sequence" set — which Benedictus has no + * equivalent of. `withPaschaltideAlleluia` applied to that fallback too, + * by analogy with the Common-category treatment above; not independently + * confirmed for this specific case. + */ +export function getMagnificatAntiphon(day: LiturgicalDay): ResolvedText { + const winner = resolveOfficeWinner(day); + if (winner.kind === 'temporal') { + const named = toResolvedText(getTemporalProper(`${winner.id}-magnificat-antiphon`)); + if (named.status.la !== 'missing' || named.status.en !== 'missing') { + return named; + } + const weekdayDefault = vespersMagnificatAntiphons[day.weekday]; + if (weekdayDefault) { + const resolved = weekdayDefault.status + ? { text: weekdayDefault.antiphon, status: weekdayDefault.status } + : verifiedText(weekdayDefault.antiphon); + return withPaschaltideAlleluia(resolved, day); + } + return named; + } + const saint = getSaintRecord(winner.id); + if (saint?.propers) { + const proper = resolveCommon(`${saint.propers}-magnificat-antiphon`); + if (proper.status.la !== 'missing' || proper.status.en !== 'missing') { + return proper; + } + } + if (saint?.benedictusCommon) { + return withPaschaltideAlleluia(resolveCommon(`magnificat-antiphon-${saint.benedictusCommon}`), day); + } + return { text: {}, status: { la: 'missing', en: 'missing' } }; +} + export function verifiedText(text: Partial>): ResolvedText { const status: Partial> = {}; for (const lang of Object.keys(text)) { diff --git a/src/hours/sext.ts b/src/hours/sext.ts index 303fc66..d55f7df 100644 --- a/src/hours/sext.ts +++ b/src/hours/sext.ts @@ -67,6 +67,9 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] { case 'suffrages': case 'lauds-preces': case 'day-collects': + case 'vespers-psalmody': + case 'vespers-office': + case 'magnificat': throw new Error(`Sext's ordo doesn't support a '${part.kind}' part`); } } diff --git a/src/hours/terce.ts b/src/hours/terce.ts index 00bb6fc..07e4a98 100644 --- a/src/hours/terce.ts +++ b/src/hours/terce.ts @@ -67,6 +67,9 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] { case 'suffrages': case 'lauds-preces': case 'day-collects': + case 'vespers-psalmody': + case 'vespers-office': + case 'magnificat': throw new Error(`Terce's ordo doesn't support a '${part.kind}' part`); } } diff --git a/src/hours/types.ts b/src/hours/types.ts index ae387c2..5bd2177 100644 --- a/src/hours/types.ts +++ b/src/hours/types.ts @@ -105,6 +105,29 @@ export type HourPart = // data/propers/common/lauds-{capitulum,responsory,hymn,versicle}-*.yml. // Expands into 4 ResolvedParts (chapter, responsory, hymn, versicle). | { kind: 'lauds-office' } + // Vespers only. The weekday-resolved 4-psalm-group psalmody — see + // hours/vespers.ts and data/hours/vespers-antiphons.yml. Opaque for the + // same reason as 'lauds-psalmody': each group's antiphon covers either + // one whole psalm, two whole psalms said together under one shared + // antiphon (Ps 115+116, Monday), or two verse-ranges of one psalm split + // under one shared antiphon (Ps 138/143, Thu/Fri) — no existing part + // kind represents that. No per-feast override mechanism yet (unlike + // Lauds' getPsalmodyOverrideFor) — every day renders the plain weekday + // default for now. + | { kind: 'vespers-psalmody' } + // Vespers only. The weekday-resolved chapter/short-responsory/hymn/ + // versicle bundle that follows the psalmody — see hours/vespers.ts and + // data/propers/common/vespers-{capitulum,responsory,hymn,versicle}-*.yml. + // Expands into 4 ResolvedParts (chapter, responsory, hymn, versicle). + | { kind: 'vespers-office' } + // Vespers only. Magnificat (Luke 1:46-55), fixed text, framed by a + // day-resolved antiphon — see hours/resolve-common.ts's + // getMagnificatAntiphon, which mirrors getBenedictusAntiphon's + // saint/temporal-id lookup but also falls back to a real weekday + // default (data/hours/vespers-magnificat-antiphons.yml, Mon-Sat) that + // Benedictus has no equivalent of. Same incipit-or-full/full-after + // framing as benedictus/nunc-dimittis, gated by isDoubleOrHigher. + | { kind: 'magnificat' } // Lauds only. Benedictus (Luke 1:68-79), fixed text (see propers/common/ // benedictus.yml) framed by a day-resolved antiphon (see hours/ // resolve-common.ts's getBenedictusAntiphon) — same incipit-or-full/ diff --git a/src/hours/vespers.ts b/src/hours/vespers.ts index 17453c6..54bdc8c 100644 --- a/src/hours/vespers.ts +++ b/src/hours/vespers.ts @@ -1,7 +1,167 @@ -import type { ResolvedOrdo } from './types'; +import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart } from './types'; +import type { LiturgicalDay, Weekday } from '../calendar/types'; +import { resolveEveningDay } from '../calendar/vespers'; +import { getDayLabel } from '../calendar/day-label'; +import { getPsalmVerses } from '../psalter'; +import { getOpeningVersicleId } from './opening-versicle'; +import { isDoubleOrHigher } from './antiphon'; +import { + resolveCommon, + getDayCollects, + getMagnificatAntiphon, + splitNamedAntiphon, + resolveOfficeWinner, + verifiedText, + seasonalOfficeSuffix, +} from './resolve-common'; +import vespersDefinitionData from '../data/hours/vespers.yml'; +import vespersAntiphonsData from '../data/hours/vespers-antiphons.yml'; -// Milestone 4, alongside Lauds — also has the first/second-Vespers overlap -// wrinkle (see OccurringFeast.vespersFrom). Not built yet. -export function resolveOrdo(date: string): ResolvedOrdo { - return { hourId: 'vespers', date, parts: [], notImplemented: true }; +const vespersDefinition = vespersDefinitionData as HourDefinition; + +type BilingualText = Partial>; +/** A whole psalm, or a `{number, verses}` verse-range slice of one — same + * shape as psalter/distribution.ts's PsalmRef, needed here (unlike Lauds' + * plain `psalms: number[]`) because two of the week's groups split a + * single psalm into two ranges under one shared antiphon — see + * vespers-antiphons.yml's own header. */ +type PsalmRefLike = number | { number: number; verses?: string }; +interface VespersGroup { + psalms: PsalmRefLike[]; + antiphon: BilingualText; +} +interface VespersDay { + groups: VespersGroup[]; +} +const vespersAntiphons = vespersAntiphonsData as Record; + +function normalizePsalmRef(ref: PsalmRefLike): { number: number; verses?: string } { + return typeof ref === 'number' ? { number: ref } : ref; +} + +/** One group's worth of psalm(s) under one shared antiphon — the antiphon + * opens (incipit-or-full) before the first psalm/verse-range and repeats + * in full once, after the last. Mirrors lauds.ts's psalmParts, extended + * for verse-range entries (see PsalmRefLike). */ +function psalmParts(group: VespersGroup, day: LiturgicalDay): ResolvedPart[] { + const { incipit, full } = splitNamedAntiphon(verifiedText(group.antiphon)); + const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit; + const refs = group.psalms.map(normalizePsalmRef); + const parts: ResolvedPart[] = refs.map((ref, i) => ({ + kind: 'psalm' as const, + psalmNumber: ref.number, + antiphon: i === 0 ? opening : undefined, + verses: getPsalmVerses(ref.number, ref.verses).map((v) => ({ n: v.n, text: v.text, status: v.status })), + })); + parts.push({ kind: 'antiphon', text: full }); + return parts; +} + +/** 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. */ +function resolvePsalmody(day: LiturgicalDay): ResolvedPart[] { + const wd = vespersAntiphons[day.weekday]; + return wd.groups.flatMap((group) => psalmParts(group, day)); +} + +function vespersCapitulumId(weekday: Weekday): string { + return weekday === 'saturday' ? 'vespers-capitulum-saturday' : 'vespers-capitulum-sunday-ferial'; +} + +function vespersResponsoryId(weekday: Weekday): string { + if (weekday === 'sunday') return 'vespers-responsory-sunday'; + if (weekday === 'saturday') return 'vespers-responsory-saturday'; + return 'vespers-responsory-ferial'; +} + +function vespersVersicleId(weekday: Weekday): string { + return weekday === 'saturday' ? 'vespers-versicle-saturday' : 'vespers-versicle-sunday-ferial'; +} + +/** The chapter/responsory/hymn/versicle bundle following the psalmody — + * see hours/types.ts's 'vespers-office' doc comment. No per-feast + * override mechanism yet, unlike Lauds' resolveOffice. Falls to a + * *seasonal* default first (Advent/Lent/Passiontide/Paschaltide — see + * resolve-common.ts's seasonalOfficeSuffix), then the plain weekday + * default. */ +function resolveOffice(day: LiturgicalDay): ResolvedPart[] { + const seasonSuffix = seasonalOfficeSuffix(day.season); + const key = seasonSuffix ?? day.weekday; + return [ + { + kind: 'chapter', + text: resolveCommon(seasonSuffix ? `vespers-capitulum-${key}` : vespersCapitulumId(day.weekday)), + }, + { + kind: 'responsory', + text: resolveCommon(seasonSuffix ? `vespers-responsory-${key}` : vespersResponsoryId(day.weekday)), + }, + { kind: 'hymn', text: resolveCommon(`vespers-hymn-${key}`) }, + { + kind: 'versicle', + text: resolveCommon(seasonSuffix ? `vespers-versicle-${key}` : vespersVersicleId(day.weekday)), + }, + ]; +} + +function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] { + switch (part.kind) { + case 'opening-versicle': + return [{ kind: 'versicle', text: resolveCommon(getOpeningVersicleId(day.season, day.winner)) }]; + case 'vespers-psalmody': + return resolvePsalmody(day); + case 'vespers-office': + return resolveOffice(day); + case 'magnificat': { + const { incipit, full } = splitNamedAntiphon(getMagnificatAntiphon(day)); + const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit; + return [ + { kind: 'canticle', canticleId: 'magnificat', text: resolveCommon('magnificat'), antiphon: opening }, + { kind: 'antiphon', text: full }, + ]; + } + case 'day-collects': + return getDayCollects(day); + case 'preces': + return [{ kind: 'preces', text: resolveCommon(part.textRef.id), label: part.label }]; + case 'versicle': + case 'chapter': + case 'responsory': + case 'prayer': + return [{ kind: part.kind, text: resolveCommon(part.textRef.id) }]; + // Not used by Vespers. + case 'hymn': + case 'lesson': + case 'psalm': + case 'canticle': + case 'martyrology': + case 'rule-reading': + case 'creed': + case 'closing-antiphon': + case 'by-day-kind': + case 'variable': + case 'day-collect': + case 'nunc-dimittis': + case 'lauds-psalmody': + case 'lauds-office': + case 'benedictus': + case 'suffrages': + case 'lauds-preces': + case 'marian-antiphon': + throw new Error(`Vespers' ordo doesn't support a '${part.kind}' part`); + } +} + +export function resolveOrdo(date: string): ResolvedOrdo { + // Every part below is an evening-hour concern, same reasoning as + // Compline — resolves off whichever day's identity actually governs + // tonight, including the cross-day commemoration folded into + // day.commemorations by resolveEveningDay itself (see calendar/ + // vespers.ts) — getDayCollects picks that up for free, no special + // handling needed here. + const day = resolveEveningDay(date); + const parts = vespersDefinition.parts.flatMap((part) => resolvePart(part, day)); + return { hourId: 'vespers', date, parts, dayLabel: getDayLabel(day) }; } diff --git a/tests/hours/prime.test.ts b/tests/hours/prime.test.ts index 2f0897f..7685b96 100644 --- a/tests/hours/prime.test.ts +++ b/tests/hours/prime.test.ts @@ -226,8 +226,8 @@ describe('resolveOrdo("prime", ...)', () => { }); describe('resolveOrdo for not-yet-built hours', () => { - it('flags vespers as not implemented rather than throwing', () => { - const ordo = resolveOrdo('vespers', MONDAY); + it('flags matins as not implemented rather than throwing', () => { + const ordo = resolveOrdo('matins', MONDAY); expect(ordo.notImplemented).toBe(true); expect(ordo.parts).toEqual([]); }); diff --git a/tests/hours/vespers.test.ts b/tests/hours/vespers.test.ts new file mode 100644 index 0000000..dcce069 --- /dev/null +++ b/tests/hours/vespers.test.ts @@ -0,0 +1,138 @@ +import { describe, expect, it } from 'vitest'; +import { resolveOrdo } from '../../src/hours'; + +describe('resolveOrdo("vespers", ...)', () => { + it('is implemented and resolves Sunday psalmody in order', () => { + // 2026-08-16 is a Sunday, in ordinary time — no First Vespers claim + // from the following Monday (a plain feria), so this stays Sunday's + // own Second Vespers. + const ordo = resolveOrdo('vespers', '2026-08-16'); + expect(ordo.notImplemented).toBeUndefined(); + expect(ordo.hourId).toBe('vespers'); + + const psalmNumbers = ordo.parts + .filter((p) => p.kind === 'psalm') + .map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined)); + expect(psalmNumbers).toEqual([109, 110, 111, 112]); + }); + + 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'); + 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]); + + // Ps 116 (the second half of the joined pair) carries no antiphon of + // its own — only Ps 115 (the group's first entry) does. + const ps115 = psalmParts[2]; + const ps116 = psalmParts[3]; + expect(ps115?.kind === 'psalm' ? ps115.antiphon : undefined).toBeDefined(); + expect(ps116?.kind === 'psalm' ? ps116.antiphon : undefined).toBeUndefined(); + }); + + it('splits Ps 138 into two verse-ranges under one shared antiphon on Thursday', () => { + // 2026-08-20 is a Thursday, in ordinary time. + const ordo = resolveOrdo('vespers', '2026-08-20'); + const psalmParts = ordo.parts.filter((p) => p.kind === 'psalm'); + const numbers = psalmParts.map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined)); + expect(numbers).toEqual([138, 138, 139, 140]); + + const [firstHalf, secondHalf] = psalmParts; + expect(firstHalf?.kind === 'psalm' ? firstHalf.verses.every((v) => v.n >= 1 && v.n <= 10) : false).toBe(true); + expect(secondHalf?.kind === 'psalm' ? secondHalf.verses.every((v) => v.n >= 11 && v.n <= 24) : false).toBe(true); + expect(secondHalf?.kind === 'psalm' ? secondHalf.antiphon : undefined).toBeUndefined(); + }); + + it("resolves Saturday's own distinct chapter, responsory, hymn, and versicle", () => { + // 2026-08-15 is a Saturday and the Assumption (Duplex I. classis) — + // well above keepsOwnSecondVespers's threshold, so it keeps its own + // Vespers (weekday stays 'saturday') rather than ceding to tomorrow. + // No per-feast Vespers-office override exists yet (see hours/ + // vespers.ts), so the plain Saturday weekday default still renders + // here regardless of the Assumption's own rank — exactly what this + // test wants to check. + const ordo = resolveOrdo('vespers', '2026-08-15'); + const chapter = ordo.parts.find((p) => p.kind === 'chapter'); + expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Rom 11:33'); + + const hymn = ordo.parts.find((p) => p.kind === 'hymn'); + expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('O lux beáta Trínitas'); + + const versicle = ordo.parts.find((p) => p.kind === 'versicle' && p.text.text.la?.includes('Vespertína')); + expect(versicle).toBeDefined(); + }); + + it('falls back to the classic weekday-default Magnificat antiphon on a bare ferial', () => { + // 2026-06-16 is a Tuesday, plain ordinary trinitytide, no saint, no + // active octave (same date already used by calendar/vespers.test.ts's + // "does not anticipate between two plain ferias"). + const ordo = resolveOrdo('vespers', '2026-06-16'); + const magnificatIndex = ordo.parts.findIndex((p) => p.kind === 'canticle' && p.canticleId === 'magnificat'); + expect(magnificatIndex).toBeGreaterThanOrEqual(0); + const magnificat = ordo.parts[magnificatIndex]; + expect(magnificat?.kind === 'canticle' ? magnificat.text.text.la : undefined).toContain('Magníficat ✠ ánima mea'); + // The closing full-antiphon repeat is the very next part. + const closingAntiphon = ordo.parts[magnificatIndex + 1]; + expect(closingAntiphon?.kind === 'antiphon' ? closingAntiphon.text.text.la : undefined).toContain('Exsúltet'); + }); + + it('folds the cross-day Vespers commemoration into day-collects (St. James keeps Vespers, St. Anne commemorated)', () => { + // St. Christopher (same-day collision, commemorated under James + // already at the calendar level) and St. Anne (the cross-day Vespers + // commemoration from calendar/vespers.ts) are both sanctoral, so both + // render as 'preces' parts (see resolve-common.ts's + // sanctoralCommemorationPart) — only a *temporal* commemoration + // renders as 'prayer'. + const ordo = resolveOrdo('vespers', '2026-07-25'); + const collectParts = ordo.parts.filter((p) => p.kind === 'prayer' || p.kind === 'preces'); + // James's own collect ('prayer') + Christopher's + Anne's commemorations ('preces'). + expect(collectParts.length).toBeGreaterThanOrEqual(3); + const anne = ordo.parts.find((p) => p.kind === 'preces' && p.label?.includes('Anne')); + expect(anne).toBeDefined(); + }); + + it("renders Advent's seasonal chapter/hymn/versicle instead of the plain weekday default", () => { + // 2025-12-02 is a Tuesday in Advent (St. Bibiana, Simplex, merely + // commemorated — the day's own winner stays temporal either way). + const ordo = resolveOrdo('vespers', '2025-12-02'); + const chapter = ordo.parts.find((p) => p.kind === 'chapter'); + expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Gen 49:10'); + const hymn = ordo.parts.find((p) => p.kind === 'hymn'); + expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Cónditor alme síderum'); + const versicle = ordo.parts.find((p) => p.kind === 'versicle' && p.text.text.la?.includes('Roráte')); + expect(versicle).toBeDefined(); + }); + + it("renders Lent's seasonal office", () => { + // 2026-03-03 is a Tuesday in Lent, before Passiontide. + const ordo = resolveOrdo('vespers', '2026-03-03'); + const chapter = ordo.parts.find((p) => p.kind === 'chapter'); + expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Joel 2:17'); + const hymn = ordo.parts.find((p) => p.kind === 'hymn'); + expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Audi, benígne Cónditor'); + }); + + it("renders Passiontide's seasonal office, with the Gloria Patri omitted from the responsory", () => { + // 2026-03-25 is a Wednesday in Passiontide (Passion Sunday 2026-03-22 + // through Holy Saturday 2026-04-04). + const ordo = resolveOrdo('vespers', '2026-03-25'); + const chapter = ordo.parts.find((p) => p.kind === 'chapter'); + expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Jer 11:20'); + const responsory = ordo.parts.find((p) => p.kind === 'responsory'); + expect(responsory?.kind === 'responsory' ? responsory.text.text.la : undefined).not.toContain('Glória Patri'); + const hymn = ordo.parts.find((p) => p.kind === 'hymn'); + expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Vexílla Regis pródeunt'); + }); + + it("renders Paschaltide's seasonal office, sharing its chapter with Lauds", () => { + // 2026-04-20 is in eastertide (Easter 2026-04-05). + const ordo = resolveOrdo('vespers', '2026-04-20'); + const chapter = ordo.parts.find((p) => p.kind === 'chapter'); + expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Rom 6:9-10'); + const hymn = ordo.parts.find((p) => p.kind === 'hymn'); + expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Ad cenam Agni próvidi'); + const versicle = ordo.parts.find((p) => p.kind === 'versicle' && p.text.text.la?.includes('Mane nobíscum')); + expect(versicle).toBeDefined(); + }); +}); diff --git a/tests/ui/shell.test.ts b/tests/ui/shell.test.ts index 1c93e58..068acd3 100644 --- a/tests/ui/shell.test.ts +++ b/tests/ui/shell.test.ts @@ -41,12 +41,12 @@ describe('shell', () => { const root = document.getElementById('app')!; mountShell(root); - const vespersBtn = Array.from(root.querySelectorAll('[data-hour]')).find( - (btn) => btn.dataset.hour === 'vespers', + const matinsBtn = Array.from(root.querySelectorAll('[data-hour]')).find( + (btn) => btn.dataset.hour === 'matins', ); - vespersBtn?.click(); + matinsBtn?.click(); - expect(root.querySelector('.hour-view h2')?.textContent).toBe('Vespers'); + expect(root.querySelector('.hour-view h2')?.textContent).toBe('Matins'); expect(root.querySelector('.hour-view-pending')).not.toBeNull(); });