Give Advent Sundays their own Matins Nocturn I-III content
Deploy / deploy (push) Successful in 1m27s

Nocturns I-II had no seasonal override mechanism at all, and Nocturn III's
own Septuagesima-Palm override didn't cover Advent — live-verified against
the reference engine that Advent has its own seasonal antiphons/versicles
across all three nocturns, identical on all 4 Advent Sundays.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
This commit is contained in:
2026-09-01 09:07:46 -04:00
parent 20b1401919
commit c3e540f6c2
7 changed files with 347 additions and 3 deletions
+27
View File
@@ -4549,3 +4549,30 @@ Re-verifying live (one `prayMatutinum` query per Sunday of the 2026 cycle, Monas
New regression tests in `tests/hours/matins.test.ts` cover all three shapes (antiphon-only,
antiphon+canticles+versicle, antiphon+versicle-only). `npm test` (817 passed), `tsc --noEmit`,
and `npm run build` all pass.
### Advent Sunday Matins Nocturns I-III seasonal override (2026-09-01)
Follow-up, same day: the user asked whether Advent Sundays' own Nocturn III needed the same kind
of fix. Live re-check (`prayMatutinum`, Monastic Tridentinum 1617, all 4 Advent Sundays of the
2026 cycle) found the answer is yes, and bigger than just Nocturn III — Advent's own seasonal
antiphons apply to **all three nocturns**, not just Nocturn III, and identically across all 4
Sundays (no per-Sunday variation the way Septuagesima-Palm has).
- **Nocturns I-II**: psalm numbers unchanged (Ps 20-31), but every one of the 6 antiphons and
both versicles are seasonal — none of which existed anywhere in the codebase before this fix
(unlike Nocturn III, Nocturns I/II had *no* override mechanism at all; `matins.ts` used the
plain-season `matins-sunday-antiphons.yml` base unconditionally).
- **Nocturn III**: swaps the plain-season 3-canticle set (Isa 33:2-10/33:13-18, Sir 36:14-19) for
a wholly different one (Isa 40:10-17, 42:10-16, 49:7-13 — 3 new `data/scripture/isa-{40,42,49}.yml`
files), under its own Marian antiphon ("Gábriel Ángelus locútus est Maríæ...") and versicle.
New `data/hours/matins-sunday-advent-nocturn-overrides.yml` — a single season-wide block (keyed
by season, not per-Sunday `temporalId`, since all 4 Advent Sundays are identical) — plus
`hours/matins.ts`'s generalized `sundayNocturnFor(nocturn, day, temporalId)`, which now checks
Advent's season-wide override ahead of (Nocturn III only) the existing per-Sunday Septuagesima-
Palm `sundayNocturn3For` lookup; Advent and Septuagesima-Palm never overlap so there's no
precedence conflict between the two override tiers.
New regression test in `tests/hours/matins.test.ts` (`Advent Sunday Nocturns I-III seasonal
override`) asserts all 4 Advent Sundays render identically. `npm test` (818 passed),
`tsc --noEmit`, and `npm run build` all pass.
@@ -0,0 +1,65 @@
# Sunday Matins Nocturns I-III, Advent override. Unlike
# matins-sunday-nocturn3-overrides.yml (keyed per-Sunday, Septuagesima
# through Palm Sunday, each Sunday distinct), this is a single season-wide
# block applied uniformly to all 4 Advent Sundays — live-verified
# identical across advent-1..4 (2026 cycle dates 11-29, 12-06, 12-13,
# 12-20) against the local reference-engine CGI instance (`prayMatutinum`,
# Monastic Tridentinum 1617, 2026-09-01).
#
# Unlike the existing Nocturn III-only override, Advent replaces the
# antiphon *and* versicle of all three nocturns (psalm/canticle numbers
# unchanged for Nocturns I-II, Ps 20-31; Nocturn III swaps its plain-season
# 3-canticle set for a wholly different one, Isa 40:10-17 / 42:10-16 /
# 49:7-13 — new scripture chapter files authored alongside this one).
#
# hours/matins.ts's lookup applies this whenever day.season === 'advent',
# ahead of (and instead of, since Advent and Septuagesima-Palm never
# overlap) the temporalId-keyed Nocturn III override.
nocturn1:
groups:
- psalms: [20, 21]
antiphon:
la: "Véniet ecce rex, excélsus cum potestáte magna, ad salvándas gentes, allelúja."
en: "Behold, there cometh the King, the Most High, with great power, to save the nations, alleluia."
- psalms: [22, 23]
antiphon:
la: "Confortáte manus dissolútas: confortámini, dícite: Ecce Deus noster véniet, et salvábit nos, allelúja."
en: "Strengthen ye the weak hands; be strong, say: Behold, our God will come, and save us, alleluia."
- psalms: [24, 25]
antiphon:
la: "Gaudéte omnes et lætámini: ecce, qui véniet Dóminus ultiónis, addúcet retributiónem: ipse véniet, et salvábit nos."
en: "Rejoice all ye, and be glad; for behold, the Lord will come with vengeance, He will bring a recompense; He will come and save us."
versicle:
v: { la: "Ex Sion spécies decóris ejus.", en: "From Sion, from the place of his greatness." }
r: { la: "Deus noster maniféste véniet.", en: "Our God shall come manifestly." }
nocturn2:
groups:
- psalms: [26, 27]
antiphon:
la: "Gaude et lætáre fília Jerúsalem: ecce, Rex tuus véniet tibi: Sion noli timére, qui cito véniet salus tua."
en: "Rejoice greatly, O daughter of Jerusalem: behold, thy King cometh into thee, O Zion; fear not, for thy salvation cometh quickly."
- psalms: [28, 29]
antiphon:
la: "Rex noster advéniet Christus, quem Joánnes prædicávit Agnum esse ventúrum."
en: "Christ our King cometh, Whom John preached, saying: Behold the Lamb That should come!"
- psalms: [30, 31]
antiphon:
la: "Ecce, vénio cito et merces mea mecum est, dicit Dóminus: dare unicuíque secúndum ópera sua."
en: "Behold, I come quickly, saith the Lord, and My reward is with Me, to give every man according as his work shall be."
versicle:
v: { la: "Emítte Agnum, Dómine, Dominatórem terræ.", en: "Send forth, O Lord, the lamb, the ruler of the earth." }
r: { la: "De Petra desérti ad montem fíliæ Sion.", en: "From Petra of the desert, to the mount of the daughter of Sion." }
nocturn3:
canticles:
- refs: [{ book: isa, chapter: 40, verses: "10-17" }]
- refs: [{ book: isa, chapter: 42, verses: "10-16" }]
- refs: [{ book: isa, chapter: 49, verses: "7-13" }]
antiphon:
la: "Gábriel Ángelus locútus est Maríæ, dicens: Ave, grátia plena; Dóminus tecum; Benedícta tu in mulíeres."
en: "The Angel Gabriel spake unto Mary, saying: Hail, thou that art full of grace, the Lord is with thee: blessed art thou among women."
versicle:
v: { la: "Egrediétur Dóminus de loco sancto suo.", en: "The Lord will come out of his holy place." }
r: { la: "Véniet ut salvet pópulum suum.", en: "He will come to save his people." }
+60
View File
@@ -0,0 +1,60 @@
# Isaiah 40 — verses 10-17 transcribed from a live Divinum Officium query
# (Monastic Tridentinum 1617, prayMatutinum, 2026-09-01, all 4 Advent
# Sundays of the 2026 cycle — identical content on each) — see
# data/hours/matins-sunday-advent-nocturn-overrides.yml. Only the verse
# range actually used by that Nocturn III canticle is authored; the rest
# of the chapter is unauthored (empty per src/scripture's own
# "unauthored -> not present" convention).
book: isa
chapter: 40
verses:
- n: 10
text:
la: "Ecce Dóminus Deus in fortitúdine véniet, * et bráchium ejus dominábitur:"
en: "Behold the Lord God shall come with strength, * and his arm shall rule:"
status: { la: verified, en: verified }
- n: 10
text:
la: "Ecce merces ejus cum eo, * et opus illíus coram illo."
en: "Behold his reward is with him * and his work is before him."
status: { la: verified, en: verified }
- n: 11
text:
la: "Sicut pastor gregem suum pascet: in bráchio suo congregábit agnos, * et in sinu suo levábit, fœtas ipse portábit."
en: "He shall feed his flock like a shepherd: he shall gather together the lambs with his arm, * and shall take them up in his bosom, and he himself shall carry them that are with young."
status: { la: verified, en: verified }
- n: 12
text:
la: "Quis mensus est pugíllo aquas, * et cælos palmo ponderávit?"
en: "Who hath measured the waters * in the hollow of his hand, and weighed the heavens with his palm?"
status: { la: verified, en: verified }
- n: 12
text:
la: "Quis appéndit tribus dígitis molem terræ, * et librávit in póndere montes, et colles in statéra?"
en: "Who hath poised with three fingers the bulk of the earth, * and weighed the mountains in scales, and the hills in a balance?"
status: { la: verified, en: verified }
- n: 13
text:
la: "Quis adjúvit spíritum Dómini? * aut quis consiliárius ejus fuit, et osténdit illi?"
en: "Who hath forwarded the spirit of the Lord? * or who hath been his counsellor, and hath taught him?"
status: { la: verified, en: verified }
- n: 14
text:
la: "Cum quo íniit consílium, et instrúxit eum, et dócuit eum sémitam justítiæ, * et erudívit eum sciéntiam, et viam prudéntiæ osténdit illi?"
en: "With whom hath he consulted, and who hath instructed him, and taught him the path of justice, * and taught him knowledge, and shewed him the way of understanding?"
status: { la: verified, en: verified }
- n: 15
text:
la: "Ecce gentes quasi stilla sítulæ, * et quasi moméntum statéræ reputátæ sunt:"
en: "Behold the Gentiles are as a drop of a bucket, * and are counted as the smallest grain of a balance."
status: { la: verified, en: verified }
- n: 16
text:
la: "Ecce ínsulæ quasi pulvis exíguus. Et Líbanus non suffíciet ad succendéndum, * et animália ejus non suffícient ad holocáustum."
en: "Behold the islands are as a little dust. And Libanus shall not be enough to burn, * nor the beasts thereof sufficient for a burnt offering."
status: { la: verified, en: verified }
- n: 17
text:
la: "Omnes gentes quasi non sint, sic sunt coram eo, * et quasi níhilum et ináne reputátæ sunt ei."
en: "All nations are before him as if they had no being at all, * and are counted to him as nothing, and vanity."
status: { la: verified, en: verified }
+68
View File
@@ -0,0 +1,68 @@
# Isaiah 42 — verses 10-16 transcribed from a live Divinum Officium query
# (Monastic Tridentinum 1617, prayMatutinum, 2026-09-01, all 4 Advent
# Sundays of the 2026 cycle — identical content on each) — see
# data/hours/matins-sunday-advent-nocturn-overrides.yml. Only the verse
# range actually used by that Nocturn III canticle is authored.
book: isa
chapter: 42
verses:
- n: 10
text:
la: "Cantáte Dómino cánticum novum, * laus ejus ab extrémis terræ,"
en: "Sing ye to the Lord a new song, * his praise is from the ends of the earth."
status: { la: verified, en: verified }
- n: 10
text:
la: "Qui descénditis in mare, et plenitúdo ejus: * ínsulæ, et habitatóres eárum."
en: "You that go down to the sea, and all that are therein: * ye islands, and ye inhabitants of them."
status: { la: verified, en: verified }
- n: 11
text:
la: "Sublevétur desértum et civitátes ejus * in dómibus habitábit Cedar."
en: "Let the desert and the cities thereof be exalted: * Cedar shall dwell in houses:"
status: { la: verified, en: verified }
- n: 11
text:
la: "Laudáte, habitatóres Petræ: * de vértice móntium clamábunt."
en: "Ye inhabitants of Petra, give praise, * they shall cry from the top of the mountains."
status: { la: verified, en: verified }
- n: 12
text:
la: "Ponent Dómino glóriam, * et laudem ejus in ínsulis nuntiábunt."
en: "They shall give glory to the Lord, * and shall declare his praise in the islands."
status: { la: verified, en: verified }
- n: 13
text:
la: "Dóminus sicut fortis egrediétur, * sicut vir prœliátor suscitábit zelum:"
en: "The Lord shall go forth as a mighty man, * as a man of war shall he stir up zeal."
status: { la: verified, en: verified }
- n: 13
text:
la: "Vociferábitur, et clamábit: * super inimícos suos confortábitur."
en: "He shall shout and cry: * he shall prevail against his enemies."
status: { la: verified, en: verified }
- n: 14
text:
la: "Tácui semper, sílui, pátiens fui: * sicut partúriens loquar."
en: "I have always held my peace, I have kept silence, I have been patient, * I will speak now as a woman in labour."
status: { la: verified, en: verified }
- n: 14
text:
la: "Dissipábo, et absorbébo simul. * desértos fáciam montes et colles, et omne gramen eórum exsiccábo."
en: "I will destroy, and swallow up at once. * I will lay waste the mountains and hills, and will make all their grass to wither."
status: { la: verified, en: verified }
- n: 15
text:
la: "Et ponam flúmina in ínsulas, * et stagna arefáciam."
en: "And I will turn rivers into islands, * and will dry up the standing pools."
status: { la: verified, en: verified }
- n: 16
text:
la: "Et ducam cæcos in viam quam nésciunt, * et in sémitis quas ignoravérunt ambuláre eos fáciam."
en: "And I will lead the blind into the way which they know not: * and in the paths which they were ignorant of I will make them walk."
status: { la: verified, en: verified }
- n: 16
text:
la: "Ponam ténebras coram eis in lucem, * et prava in recta."
en: "I will make darkness light before them, * and crooked things straight."
status: { la: verified, en: verified }
+58
View File
@@ -0,0 +1,58 @@
# Isaiah 49 — verses 7-13 transcribed from a live Divinum Officium query
# (Monastic Tridentinum 1617, prayMatutinum, 2026-09-01, all 4 Advent
# Sundays of the 2026 cycle — identical content on each) — see
# data/hours/matins-sunday-advent-nocturn-overrides.yml. Only the verse
# range actually used by that Nocturn III canticle is authored.
book: isa
chapter: 49
verses:
- n: 7
text:
la: "Hæc dicit Dóminus redémptor Israël, sanctus ejus, ad contemptíbilem ánimam, * ad abominátam gentem, ad servum dominórum."
en: "Thus saith the Lord the redeemer of Israel, his Holy One, to the soul that is despised, * to the nation that is abhorred, to the servant of rulers."
status: { la: verified, en: verified }
- n: 7
text:
la: "Reges vidébunt, et consúrgent príncipes, et adorábunt propter Dóminum, quia fidélis est, * et sanctum Israël qui elégit te."
en: "Kings shall see, and princes shall rise up, and adore for the Lords sake, because he is faithful, * and for the Holy One of Israel, who hath chosen thee."
status: { la: verified, en: verified }
- n: 8
text:
la: "Hæc dicit Dóminus: In témpore plácito exaudívi te, * et in die salútis auxiliátus sum tui."
en: "Thus saith the Lord: In an acceptable time I have heard thee, * and in the day of salvation I have helped thee."
status: { la: verified, en: verified }
- n: 8
text:
la: "Et servávi te, et dedi te in fœdus pópuli, * ut suscitáres terram, et possidéres hereditátes dissipátas:"
en: "And I have preserved thee, and given thee to be a covenant of the people, * that thou mightest raise up the earth, and possess the inheritances that were destroyed:"
status: { la: verified, en: verified }
- n: 9
text:
la: "Ut díceres his qui vincti sunt: exíte, * et his qui in ténebris: revelámini."
en: "That thou mightest say to them that are bound: Come forth: * and to them that are in darkness: Shew yourselves."
status: { la: verified, en: verified }
- n: 9
text:
la: "Super vias pascéntur, * et in ómnibus planis páscua eórum."
en: "They shall feed in the ways, * and their pastures shall be in every plain."
status: { la: verified, en: verified }
- n: 10
text:
la: "Non esúrient neque sítient, et non percútiet eos æstus et sol, * quia miserátor eórum reget eos, et ad fontes aquárum potábit eos."
en: "They shall not hunger, nor thirst, neither shall the heat nor the sun strike them: * for he that is merciful to them, shall be their shepherd, and at the fountains of waters he shall give them drink."
status: { la: verified, en: verified }
- n: 11
text:
la: "Et ponam omnes montes meos in viam, * et sémitæ meæ exaltabúntur."
en: "And I will make all my mountains a way, * and my paths shall be exalted."
status: { la: verified, en: verified }
- n: 12
text:
la: "Ecce isti de longe vénient, et ecce illi ab aquilóne et mari, * et isti de terra austráli."
en: "Behold these shall come from afar, and behold these from the north and from the sea, * and these from the south country."
status: { la: verified, en: verified }
- n: 13
text:
la: "Laudáte cæli, et exsúlta terra, jubiláte montes laudem: * quia consolátus est Dóminus pópulum suum, et páuperum suórum miserébitur."
en: "Give praise, O ye heavens, and rejoice, O earth, ye mountains, give praise with jubilation: * because the Lord hath comforted his people, and will have mercy on his poor ones."
status: { la: verified, en: verified }
+33 -3
View File
@@ -95,6 +95,7 @@ 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 matinsSundayAdventOverridesData from '../data/hours/matins-sunday-advent-nocturn-overrides.yml';
import matinsFerialAntiphonsData from '../data/hours/matins-ferial-antiphons.yml';
import type { Weekday } from '../calendar/types';
@@ -145,6 +146,35 @@ function sundayNocturn3For(temporalId: string | undefined): SundayNocturn {
};
}
// Advent's own Nocturns I-III override, applied uniformly across all 4
// Advent Sundays (unlike sundayNocturn3Overrides above, which is keyed
// per-distinct-Sunday for Septuagesima-Palm) — see
// data/hours/matins-sunday-advent-nocturn-overrides.yml's own header.
interface MatinsSundayAdventOverrides {
nocturn1: Partial<SundayNocturn>;
nocturn2: Partial<SundayNocturn>;
nocturn3: Partial<SundayNocturn>;
}
const sundayAdventOverrides = matinsSundayAdventOverridesData as unknown as MatinsSundayAdventOverrides;
/** Sunday Nocturn I/II/III content for a given day — Advent's own
* season-wide override (all three nocturns) takes precedence over the
* plain-season default; Nocturn III additionally checks the per-Sunday
* Septuagesima-Palm override (`sundayNocturn3For`) when Advent doesn't
* apply (the two never overlap). */
function sundayNocturnFor(nocturnKey: 'nocturn1' | 'nocturn2' | 'nocturn3', day: LiturgicalDay, temporalId: string | undefined): SundayNocturn {
const base = nocturnKey === 'nocturn3' ? sundayNocturn3For(temporalId) : sundayAntiphons[nocturnKey];
if (day.season !== 'advent') return base;
const override = sundayAdventOverrides[nocturnKey];
if (!override) return base;
return {
groups: override.groups ?? base.groups,
canticles: override.canticles ?? base.canticles,
antiphon: override.antiphon ?? base.antiphon,
versicle: override.versicle ?? base.versicle,
};
}
interface FerialGroup {
psalms: PsalmRef[];
antiphon: BilingualText;
@@ -704,13 +734,13 @@ export function resolveOrdo(date: string): ResolvedOrdo {
if (threeNocturns && day.weekday === 'sunday') {
parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[0]}` });
parts.push(...sundayPsalmNocturn(sundayAntiphons.nocturn1, day));
parts.push(...sundayPsalmNocturn(sundayNocturnFor('nocturn1', day, temporalId), day));
parts.push(...(nocturn1Readings ?? []));
parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[1]}` });
parts.push(...sundayPsalmNocturn(sundayAntiphons.nocturn2, day));
parts.push(...sundayPsalmNocturn(sundayNocturnFor('nocturn2', day, temporalId), day));
parts.push(...(nocturn2Readings ?? []));
parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[2]}` });
parts.push(...sundayCanticleNocturn(sundayNocturn3For(temporalId), day));
parts.push(...sundayCanticleNocturn(sundayNocturnFor('nocturn3', day, temporalId), day));
parts.push(...(nocturn3Readings ?? []));
parts.push({ kind: 'te-deum', text: resolveCommon('te-deum') });
parts.push({ kind: 'versicle', text: resolveCommon('te-decet-laus') });
+36
View File
@@ -237,6 +237,42 @@ describe('resolveOrdo("matins", ...) Sunday Nocturn III seasonal override (2026-
});
});
// Real bug (found 2026-09-01, same shape as the Nocturn III fix above, but
// bigger: Advent Sundays' Nocturns I-II antiphons/versicles are also
// seasonal, not just Nocturn III's antiphon+canticles+versicle — live-
// verified against the reference engine (Monastic Tridentinum 1617),
// identical across all 4 Advent Sundays of the 2026 cycle.
describe('resolveOrdo("matins", ...) Advent Sunday Nocturns I-III seasonal override (2026-09-01 fix)', () => {
it('all 4 Advent Sundays render identical seasonal content (Nocturns I-III antiphons/versicles, Nocturn III canticles)', () => {
const dates = ['2026-11-29', '2026-12-06', '2026-12-13', '2026-12-20'];
const renders = dates.map((date) => resolveOrdo('matins', date));
for (const ordo of renders) {
const psalms = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
expect(psalms).toEqual([3, 94, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]);
const canticles = ordo.parts.filter((p) => p.kind === 'canticle') as { canticleId: string }[];
expect(canticles.map((c) => c.canticleId)).toEqual(['isa-40-10-17', 'isa-42-10-16', 'isa-49-7-13']);
const antiphons = ordo.parts.filter((p) => p.kind === 'antiphon') as { text: { text: Record<string, string> } }[];
expect(antiphons.at(-1)?.text.text.la).toBe(
'Ant. Gábriel Ángelus locútus est Maríæ, dicens: Ave, grátia plena; Dóminus tecum; Benedícta tu in mulíeres.',
);
const versicles = ordo.parts.filter((p) => p.kind === 'versicle') as { text: { text: Record<string, string> } }[];
const [, nocturn1, nocturn2, nocturn3] = versicles;
expect(nocturn1?.text.text.la).toBe('V. Ex Sion spécies decóris ejus.\nR. Deus noster maniféste véniet.');
expect(nocturn2?.text.text.la).toBe('V. Emítte Agnum, Dómine, Dominatórem terræ.\nR. De Petra desérti ad montem fíliæ Sion.');
expect(nocturn3?.text.text.la).toBe('V. Egrediétur Dóminus de loco sancto suo.\nR. Véniet ut salvet pópulum suum.');
}
// Same content on every one of the 4 Sundays, not just individually correct.
const nocturn1Openings = renders.map(
(ordo) => (ordo.parts.filter((p) => p.kind === 'antiphon')[1] as { text: { text: Record<string, string> } }).text.text.la,
);
expect(new Set(nocturn1Openings).size).toBe(1);
});
});
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