Fix Sunday Matins nocturn versicles dropping their R. line
Deploy / deploy (push) Successful in 1m13s

sundayPsalmNocturn/sundayCanticleNocturn only ever rendered a
versicle's V. line, silently missing the responsory for Sunday Matins
and every Duplex+ weekday feast using matins-psalmody-overrides. Both
now share the versicleText helper introduced for the ferial nocturn
antiphons, which combines V./R. into one block.
This commit is contained in:
2026-08-21 09:31:09 -04:00
parent 675b644c68
commit 025d0e3a08
3 changed files with 37 additions and 19 deletions
+12 -6
View File
@@ -2229,10 +2229,16 @@ New `data/hours/matins-ferial-antiphons.yml` (6 weekdays, 5-6 antiphon groups ea
group per weekday carrying a versicle) + `hours/matins.ts`'s new `ferialAntiphonedNocturn`, group per weekday carrying a versicle) + `hours/matins.ts`'s new `ferialAntiphonedNocturn`,
replacing the old bare `ferialPsalmody` call in the plain-ferial branch (`ferialPsalmody` itself replacing the old bare `ferialPsalmody` call in the plain-ferial branch (`ferialPsalmody` itself
stays, still used by `ferialPsalmodyThreeNocturns`'s intentionally antiphon-less fallback). stays, still used by `ferialPsalmodyThreeNocturns`'s intentionally antiphon-less fallback).
Versicle rendered as one combined V./R. block (`"V. ...\nR. ..."`), not just the V. line — Versicle rendered as one combined V./R. block (`"V. ...\nR. ..."`) via a new shared
`sundayPsalmNocturn`'s own versicle handling drops the R. line entirely (only pushes `versicleText` helper.
`group.versicle.v`), which looks like a pre-existing real bug there, not replicated here on
purpose; not yet fixed, since it's Sunday's own code, out of scope for this pass. **Follow-up, same day: fixed the pre-existing Sunday versicle bug too.** `sundayPsalmNocturn`/
`sundayCanticleNocturn` (used by both Sunday Matins and every Duplex+ weekday feast's
`matins-psalmody-overrides` content) only ever pushed `group.versicle.v` — the V. line — never
the R. line, so every Sunday/override nocturn versicle has been silently missing its responsory
since the Sunday psalmody was first authored. Both functions now go through the same
`versicleText` helper the ferial nocturn code above uses. `tests/hours/matins.test.ts` gained a
regression test asserting all 3 Sunday nocturn versicles render both lines.
`tests/hours/matins.test.ts` gained a new assertion (on the existing `FERIAL_DATE` proof, `tests/hours/matins.test.ts` gained a new assertion (on the existing `FERIAL_DATE` proof,
2026-12-01, Tuesday of Advent I) confirming every nocturn psalm gets its own antiphon, 2026-12-01, Tuesday of Advent I) confirming every nocturn psalm gets its own antiphon,
@@ -2240,8 +2246,8 @@ correctly grouped, and exactly one versicle appears at the right point. Also fix
Triduum tests that asserted "no versicle part anywhere in the whole ordo" — true only because Triduum tests that asserted "no versicle part anywhere in the whole ordo" — true only because
ferial nocturns had no versicles at all before this change; now checks specifically that the ferial nocturns had no versicles at all before this change; now checks specifically that the
dropped *opening* versicle (and Ps 3, which it precedes) is gone, via `ordo.parts[0]` being the dropped *opening* versicle (and Ps 3, which it precedes) is gone, via `ordo.parts[0]` being the
nocturn's own first psalm, rather than asserting zero versicles exist. `npm test` (420 tests) nocturn's own first psalm, rather than asserting zero versicles exist. `npm test` (421 tests,
and `tsc --noEmit` both pass. including the Sunday-versicle regression test above) and `tsc --noEmit` both pass.
## Known, deliberate simplifications (not bugs — working as designed) ## Known, deliberate simplifications (not bugs — working as designed)
+15 -13
View File
@@ -129,6 +129,17 @@ const ferialAntiphons = matinsFerialAntiphonsData as unknown as MatinsFerialAnti
const NOCTURN_NUMERAL = ['I', 'II', 'III'] as const; const NOCTURN_NUMERAL = ['I', 'II', 'III'] as const;
/** Combines a versicle's V. and R. lines into one rendered block — every
* nocturn versicle in this file goes through this (previously
* `sundayPsalmNocturn`/`sundayCanticleNocturn` only rendered the V. line,
* silently dropping the responsory; fixed here, 2026-08-21). */
function versicleText(versicle: { v: BilingualText; r: BilingualText }): ResolvedText {
return verifiedText({
la: `V. ${versicle.v.la}\nR. ${versicle.r.la}`,
en: `V. ${versicle.v.en}\nR. ${versicle.r.en}`,
});
}
type PsalmPart = Extract<ResolvedPart, { kind: 'psalm' }>; type PsalmPart = Extract<ResolvedPart, { kind: 'psalm' }>;
function plainPsalm(number: number): PsalmPart { function plainPsalm(number: number): PsalmPart {
@@ -174,7 +185,7 @@ function sundayPsalmNocturn(group: SundayNocturn, day: LiturgicalDay): ResolvedP
}); });
parts.push({ kind: 'antiphon', text: full }); parts.push({ kind: 'antiphon', text: full });
} }
parts.push({ kind: 'versicle', text: verifiedText({ la: group.versicle.v.la, en: group.versicle.v.en }) }); parts.push({ kind: 'versicle', text: versicleText(group.versicle) });
return parts; return parts;
} }
@@ -197,7 +208,7 @@ function sundayCanticleNocturn(group: SundayNocturn, day: LiturgicalDay): Resolv
}; };
}); });
parts.push({ kind: 'antiphon', text: full }); parts.push({ kind: 'antiphon', text: full });
parts.push({ kind: 'versicle', text: verifiedText(group.versicle.v) }); parts.push({ kind: 'versicle', text: versicleText(group.versicle) });
return parts; return parts;
} }
@@ -241,9 +252,7 @@ function ferialPsalmody(day: LiturgicalDay): ResolvedPart[] {
* per-weekday psalm set). Every psalm-group carries its own antiphon * per-weekday psalm set). Every psalm-group carries its own antiphon
* (incipit-or-full opening, same rank rule as Sunday's own nocturns), and * (incipit-or-full opening, same rank rule as Sunday's own nocturns), and
* exactly one group per weekday carries a versicle+responsory, rendered * exactly one group per weekday carries a versicle+responsory, rendered
* as one combined V./R. block (not just the V. line — see the sibling * as one combined V./R. block via `versicleText`. */
* `sundayPsalmNocturn`'s own versicle handling, which drops the R. line
* entirely; not replicated here on purpose). */
function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] { function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] {
const nocturn = ferialAntiphons[day.weekday as Exclude<Weekday, 'sunday'>]; const nocturn = ferialAntiphons[day.weekday as Exclude<Weekday, 'sunday'>];
const isDouble = isDoubleOrHigher(resolveOfficeWinner(day)); const isDouble = isDoubleOrHigher(resolveOfficeWinner(day));
@@ -261,14 +270,7 @@ function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] {
}); });
parts.push({ kind: 'antiphon', text: full }); parts.push({ kind: 'antiphon', text: full });
if (group.versicle) { if (group.versicle) {
const { v, r } = group.versicle; parts.push({ kind: 'versicle', text: versicleText(group.versicle) });
parts.push({
kind: 'versicle',
text: verifiedText({
la: `V. ${v.la}\nR. ${r.la}`,
en: `V. ${v.en}\nR. ${r.en}`,
}),
});
} }
} }
return parts; return parts;
+10
View File
@@ -123,6 +123,16 @@ describe('resolveOrdo("matins", ...) Sunday (3-nocturn) branch', () => {
expect(ordo.parts.some((p) => p.kind === 'te-deum')).toBe(true); expect(ordo.parts.some((p) => p.kind === 'te-deum')).toBe(true);
}); });
it('renders each nocturn versicle with both its V. and R. lines (real bug: sundayPsalmNocturn/sundayCanticleNocturn used to drop the R. line entirely)', () => {
const versicles = ordo.parts.filter((p) => p.kind === 'versicle') as { text: { text: Record<string, string> } }[];
// Opening Deus-in-adjutorium versicle + one per nocturn (1, 2, 3).
expect(versicles).toHaveLength(4);
const [, nocturn1, nocturn2, nocturn3] = versicles;
expect(nocturn1?.text.text.la).toBe('V. Memor fui nocte nóminis tui Dómine.\nR. Et custodívi legem tuam.');
expect(nocturn2?.text.text.la).toBe('V. Média nocte surgébam ad confiténdum tibi.\nR. Super justítia justificatiónis tuæ.');
expect(nocturn3?.text.text.la).toBe('V. Exaltáre Dómine in virtúte tua.\nR. Cantábimus et psallémus virtútes tuas.');
});
it("resolves the user's own bible-plan reading (Tobit/Sirach) for Nocturn 1", () => { it("resolves the user's own bible-plan reading (Tobit/Sirach) for Nocturn 1", () => {
const lessons = ordo.parts.filter((p) => p.kind === 'lesson'); const lessons = ordo.parts.filter((p) => p.kind === 'lesson');
const citations = lessons.map((l) => (l as { text: { citation?: { en?: string } } }).text.citation?.en); const citations = lessons.map((l) => (l as { text: { citation?: { en?: string } } }).text.citation?.en);