Fix Sunday Matins nocturn versicles dropping their R. line
Deploy / deploy (push) Successful in 1m13s
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:
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user