From 3d221c2a45b5818c88afb298951c584b767ba2d8 Mon Sep 17 00:00:00 2001 From: Will Estes Date: Tue, 1 Sep 2026 08:17:14 -0400 Subject: [PATCH] Give Sunday Matins Nocturn III its own antiphon Septuagesima-Palm Sunday MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It was hardcoded to "Allelúja, allelúja, allelúja" year-round. Each of the 9 Sundays from Septuagesima through Palm Sunday has its own Gospel-tied antiphon instead, live-verified against Monastic Tridentinum 1617; Lent 1-4 also swap in their own canticles, and Lent 1-4 plus Passion/Palm Sunday swap in their own versicle. New matins-sunday-nocturn3-overrides.yml, keyed by temporalId and merged over the base nocturn3 group by hours/matins.ts's new sundayNocturn3For — only the real-Sunday branch uses it. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q --- .../matins-sunday-nocturn3-overrides.yml | 119 ++++++++++++++++++ src/hours/matins.ts | 21 +++- tests/hours/matins.test.ts | 49 ++++++++ 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 src/data/hours/matins-sunday-nocturn3-overrides.yml diff --git a/src/data/hours/matins-sunday-nocturn3-overrides.yml b/src/data/hours/matins-sunday-nocturn3-overrides.yml new file mode 100644 index 0000000..f39991d --- /dev/null +++ b/src/data/hours/matins-sunday-nocturn3-overrides.yml @@ -0,0 +1,119 @@ +# Sunday Matins Nocturn III per-season override, keyed by temporalId. +# matins-sunday-antiphons.yml's own [Daym0] antiphon ("Allelúja, allelúja, +# allelúja") is the plain-season default; from Septuagesima through Palm +# Sunday inclusive (9 Sundays), Monastic Tridentinum 1617 gives Nocturn +# III its own distinct, Gospel-tied antiphon instead — live-verified +# against the local reference-engine CGI instance (`prayMatutinum`, +# Monastic Tridentinum 1617, 2026-09-01, one query per Sunday of 2026's +# cycle). +# +# That same live check found the original TODO note incomplete: it isn't +# only the antiphon that changes. +# - Septuagesima, Sexagesima, Quinquagesima keep the base canticles +# (Isa 33:2-10 / Isa 33:13-18 / Sir 36:14-19) and the base versicle +# ("Exaltáre Dómine in virtúte tua. / Cantábimus et psallémus virtútes +# tuas.") — only `antiphon` is listed here for these three. +# - Lent 1-4 substitute a wholly different set of 3 canticles (Jer +# 14:17-21, Lam 5:1-7,15-17,19-21, Ezek 36:24-28 — new scripture +# chapter files authored alongside this one) and a different versicle +# ("Scuto circúmdabit te véritas ejus. / Non timébis a timóre +# noctúrno."). +# - Passion Sunday and Palm Sunday keep the base canticles but take the +# Lent versicle's own neighbor verse instead ("Ne perdas cum ímpiis, +# Deus, ánimam meam. / Et cum viris sánguinum vitam meam."). +# +# `canticles`/`versicle` are omitted where the base +# matins-sunday-antiphons.yml value already applies; hours/matins.ts's +# lookup falls back to the base nocturn3 group for any field not given +# here. + +septuagesima: + antiphon: + la: "Ite et vos in víneam meam, † et quod justum fúerit, dabo vobis." + en: "Go ye also into my vineyard, and whatsoever is right, that will I give you." + +sexagesima: + antiphon: + la: "Semen cécidit in terram bonam, † et ortum fecit fructum céntuplum." + en: "Some seed fell on good ground, and gave fruit one hundred-fold." + +quinquagesima: + antiphon: + la: "Miserére mei, Fili David! † Quid vis ut fáciam tibi? Dómine, ut vídeam." + en: "Son of David, have mercy on me. What wilt thou that I do to thee? Lord, that I may see." + +lent-1: + antiphon: + la: "Non in solo pane vivit homo, † sed in omni verbo quod procédit de ore Dei." + en: "Not on bread alone doth man live, but on every word that proceedeth from the mouth of God." + canticles: + - refs: [{ book: jer, chapter: 14, verses: "17-21" }] + - refs: + - { book: lam, chapter: 5, verses: "1-7" } + - { book: lam, chapter: 5, verses: "15-17" } + - { book: lam, chapter: 5, verses: "19-21" } + - refs: [{ book: ezek, chapter: 36, verses: "24-28" }] + versicle: + v: { la: "Scuto circúmdabit te véritas ejus.", en: "His truth shall compass thee with a shield." } + r: { la: "Non timébis a timóre noctúrno.", en: "Thou shalt not be afraid of the terror of the night." } + +lent-2: + antiphon: + la: "Faciámus hic tria tabernácula: † tibi unum, Móysi unum, et Elíæ unum." + en: "Let us make here three tabernacles, one for thee, and one for Moses, and one for Elias." + canticles: + - refs: [{ book: jer, chapter: 14, verses: "17-21" }] + - refs: + - { book: lam, chapter: 5, verses: "1-7" } + - { book: lam, chapter: 5, verses: "15-17" } + - { book: lam, chapter: 5, verses: "19-21" } + - refs: [{ book: ezek, chapter: 36, verses: "24-28" }] + versicle: + v: { la: "Scuto circúmdabit te véritas ejus.", en: "His truth shall compass thee with a shield." } + r: { la: "Non timébis a timóre noctúrno.", en: "Thou shalt not be afraid of the terror of the night." } + +lent-3: + antiphon: + la: "Si in dígito Dei ejício dæmónia, profécto pervénit in vos regnum Dei." + en: "If I by the finger of God cast out devils; doubtless the kingdom of God is come upon you." + canticles: + - refs: [{ book: jer, chapter: 14, verses: "17-21" }] + - refs: + - { book: lam, chapter: 5, verses: "1-7" } + - { book: lam, chapter: 5, verses: "15-17" } + - { book: lam, chapter: 5, verses: "19-21" } + - refs: [{ book: ezek, chapter: 36, verses: "24-28" }] + versicle: + v: { la: "Scuto circúmdabit te véritas ejus.", en: "His truth shall compass thee with a shield." } + r: { la: "Non timébis a timóre noctúrno.", en: "Thou shalt not be afraid of the terror of the night." } + +lent-4: + antiphon: + la: "Satiávit Dóminus quinque míllia hóminum de quinque pánibus et duóbus píscibus." + en: "The Lord filled up five thousand men with five loaves of bread and two fish." + canticles: + - refs: [{ book: jer, chapter: 14, verses: "17-21" }] + - refs: + - { book: lam, chapter: 5, verses: "1-7" } + - { book: lam, chapter: 5, verses: "15-17" } + - { book: lam, chapter: 5, verses: "19-21" } + - refs: [{ book: ezek, chapter: 36, verses: "24-28" }] + versicle: + v: { la: "Scuto circúmdabit te véritas ejus.", en: "His truth shall compass thee with a shield." } + r: { la: "Non timébis a timóre noctúrno.", en: "Thou shalt not be afraid of the terror of the night." } + +passion-sunday: + antiphon: + la: "Ego glóriam meam non quæro: † est qui quærat, et júdicet." + en: "I seek not Mine Own glory; there is One That seeketh and judgeth." + versicle: + v: { la: "Ne perdas cum ímpiis, Deus, ánimam meam.", en: "Take not away my soul, O God, with the wicked." } + r: { la: "Et cum viris sánguinum vitam meam.", en: "Nor my life with bloody men." } + +palm-sunday: + antiphon: + la: "Hosánna fílio David: † benedíctus, qui venit in nómine Dómini, Rex Israël: † Hosánna in excélsis." + en: "Hosanna to the Son of David! Blessed is He that cometh in the Name of the Lord. O King of Israel: Hosanna in the highest!" + versicle: + v: { la: "Ne perdas cum ímpiis, Deus, ánimam meam.", en: "Take not away my soul, O God, with the wicked." } + r: { la: "Et cum viris sánguinum vitam meam.", en: "Nor my life with bloody men." } diff --git a/src/hours/matins.ts b/src/hours/matins.ts index 39c4e5c..aedafdd 100644 --- a/src/hours/matins.ts +++ b/src/hours/matins.ts @@ -94,6 +94,7 @@ import { getNocturnReadings, type NocturnReading } from '../propers/nocturn-read import { getOctaveReading } from '../propers/octave-readings'; import { getMatinsSaintOverride, getMatinsCommonOverride, type MatinsPsalmodyScheme } from './matins-psalmody-overrides'; import matinsSundayAntiphonsData from '../data/hours/matins-sunday-antiphons.yml'; +import matinsSundayNocturn3OverridesData from '../data/hours/matins-sunday-nocturn3-overrides.yml'; import matinsFerialAntiphonsData from '../data/hours/matins-ferial-antiphons.yml'; import type { Weekday } from '../calendar/types'; @@ -126,6 +127,24 @@ interface MatinsSundayAntiphons { } const sundayAntiphons = matinsSundayAntiphonsData as unknown as MatinsSundayAntiphons; +// Septuagesima through Palm Sunday inclusive: Nocturn III takes a +// per-Sunday antiphon (and, for some of these Sundays, its own canticles/ +// versicle too) instead of the plain-season "Allelúja" default — see +// data/hours/matins-sunday-nocturn3-overrides.yml's own header. +type SundayNocturn3Overrides = Record>; +const sundayNocturn3Overrides = matinsSundayNocturn3OverridesData as unknown as SundayNocturn3Overrides; + +function sundayNocturn3For(temporalId: string | undefined): SundayNocturn { + const base = sundayAntiphons.nocturn3; + const override = temporalId ? sundayNocturn3Overrides[temporalId] : undefined; + if (!override) return base; + return { + canticles: override.canticles ?? base.canticles, + antiphon: override.antiphon ?? base.antiphon, + versicle: override.versicle ?? base.versicle, + }; +} + interface FerialGroup { psalms: PsalmRef[]; antiphon: BilingualText; @@ -691,7 +710,7 @@ export function resolveOrdo(date: string): ResolvedOrdo { parts.push(...sundayPsalmNocturn(sundayAntiphons.nocturn2, day)); parts.push(...(nocturn2Readings ?? [])); parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[2]}` }); - parts.push(...sundayCanticleNocturn(sundayAntiphons.nocturn3, day)); + parts.push(...sundayCanticleNocturn(sundayNocturn3For(temporalId), day)); parts.push(...(nocturn3Readings ?? [])); parts.push({ kind: 'te-deum', text: resolveCommon('te-deum') }); parts.push({ kind: 'versicle', text: resolveCommon('te-decet-laus') }); diff --git a/tests/hours/matins.test.ts b/tests/hours/matins.test.ts index ce5101f..c5e125b 100644 --- a/tests/hours/matins.test.ts +++ b/tests/hours/matins.test.ts @@ -188,6 +188,55 @@ describe('resolveOrdo("matins", ...) Sunday (3-nocturn) branch', () => { }); }); +// Real bug (found 2026-09-01, see TODO.md/vu_ferial_matins_antiphons.md): +// Sunday Matins Nocturn III's antiphon was hardcoded to "Allelúja, allelúja, +// allelúja" year-round; Septuagesima through Palm Sunday inclusive each +// have their own Gospel-tied antiphon instead, live-verified against the +// reference engine (Monastic Tridentinum 1617, 2026 calendar) — and for +// Lent 1-4 the canticles and versicle differ too, not just the antiphon. +describe('resolveOrdo("matins", ...) Sunday Nocturn III seasonal override (2026-09-01 fix)', () => { + it('Septuagesima: proper antiphon, base canticles/versicle unchanged', () => { + const ordo = resolveOrdo('matins', '2026-02-01'); + const canticles = ordo.parts.filter((p) => p.kind === 'canticle') as { canticleId: string }[]; + expect(canticles.map((c) => c.canticleId)).toEqual(['isa-33-2-10', 'isa-33-13-18', 'sir-36-14-19']); + const antiphons = ordo.parts.filter((p) => p.kind === 'antiphon') as { text: { text: Record } }[]; + expect(antiphons.at(-1)?.text.text.la).toBe('Ant. Ite et vos in víneam meam, † et quod justum fúerit, dabo vobis.'); + const versicles = ordo.parts.filter((p) => p.kind === 'versicle') as { text: { text: Record } }[]; + expect(versicles.at(-2)?.text.text.la).toBe('V. Exaltáre Dómine in virtúte tua.\nR. Cantábimus et psallémus virtútes tuas.'); + }); + + it('Lent 1: proper antiphon, proper canticles (Jer 14/Lam 5/Ezek 36), proper versicle', () => { + const ordo = resolveOrdo('matins', '2026-02-22'); + const canticles = ordo.parts.filter((p) => p.kind === 'canticle') as { canticleId: string; text: { text: Record } }[]; + expect(canticles.map((c) => c.canticleId)).toEqual([ + 'jer-14-17-21', + 'lam-5-1-7_lam-5-15-17_lam-5-19-21', + 'ezek-36-24-28', + ]); + expect(canticles.every((c) => c.text.text.la && c.text.text.en)).toBe(true); + const antiphons = ordo.parts.filter((p) => p.kind === 'antiphon') as { text: { text: Record } }[]; + expect(antiphons.at(-1)?.text.text.la).toBe('Ant. Non in solo pane vivit homo, † sed in omni verbo quod procédit de ore Dei.'); + const versicles = ordo.parts.filter((p) => p.kind === 'versicle') as { text: { text: Record } }[]; + expect(versicles.at(-2)?.text.text.la).toBe('V. Scuto circúmdabit te véritas ejus.\nR. Non timébis a timóre noctúrno.'); + }); + + it('Passion Sunday and Palm Sunday: base canticles, but the "Ne perdas" versicle, not the base "Exaltáre"', () => { + for (const date of ['2026-03-22', '2026-03-29']) { + const ordo = resolveOrdo('matins', date); + const canticles = ordo.parts.filter((p) => p.kind === 'canticle') as { canticleId: string }[]; + expect(canticles.map((c) => c.canticleId)).toEqual(['isa-33-2-10', 'isa-33-13-18', 'sir-36-14-19']); + const versicles = ordo.parts.filter((p) => p.kind === 'versicle') as { text: { text: Record } }[]; + expect(versicles.at(-2)?.text.text.la).toBe('V. Ne perdas cum ímpiis, Deus, ánimam meam.\nR. Et cum viris sánguinum vitam meam.'); + } + const passionOrdo = resolveOrdo('matins', '2026-03-22'); + const palmOrdo = resolveOrdo('matins', '2026-03-29'); + const antiphonLa = (ordo: ReturnType) => + (ordo.parts.filter((p) => p.kind === 'antiphon').at(-1) as { text: { text: Record } }).text.text.la; + expect(antiphonLa(passionOrdo)).toBe('Ant. Ego glóriam meam non quæro: † est qui quærat, et júdicet.'); + expect(antiphonLa(palmOrdo)).toBe('Ant. Hosánna fílio David: † benedíctus, qui venit in nómine Dómini, Rex Israël: † Hosánna in excélsis.'); + }); +}); + describe('resolveOrdo("matins", ...) Duplex+ weekday-feast (3-nocturn, non-Sunday) branch', () => { // 2026-08-17 -- St. Lawrence's own octave closing day ("In Octava S. // Laurentii Martyris ~ Duplex"), live-verified directly against the