Add antiphons and versicle to plain ferial weekday Matins nocturns
Deploy / deploy (push) Successful in 1m13s

Every non-Sunday, non-Duplex+ Matins nocturn rendered bare psalms with
no antiphon at all. Sourced each psalm's antiphon from Divinum
Officium's Monastic Matins psalter and regrouped to fit
psalter-distribution.yml's own already-established (and non-historical)
weekday psalm redistribution, since the source's own per-weekday
antiphon pairings don't survive that reslice intact.
This commit is contained in:
2026-08-21 09:27:13 -04:00
parent 6fedcd7f2b
commit 675b644c68
4 changed files with 375 additions and 2 deletions
+42
View File
@@ -2201,6 +2201,48 @@ saint-name). `tests/hours/compline.test.ts` and `tests/ui/shell.test.ts` updated
test` (383 tests) and `tsc --noEmit` both pass; manually verified in the browser (English, test` (383 tests) and `tsc --noEmit` both pass; manually verified in the browser (English,
Latin, and both-language modes) against an Advent date with an active octave. Latin, and both-language modes) against an Advent date with an active octave.
### Ferial weekday Matins nocturn antiphons + versicle — done (2026-08-21)
Real gap found (user flagged it directly): the plain ferial weekday Matins nocturn
(`hours/matins.ts`'s single-nocturn branch, no Sunday/Duplex+ override) rendered bare psalms
with no antiphon at all — not a documented design choice like `ferialPsalmodyThreeNocturns`'s
antiphon-less fallback, just an oversight from before antiphons existed anywhere in this hour.
Source: Divinum Officium's Monastic-labeled Matins psalter, `Latin/Psalterium/Psalmi/Psalmi
matutinum.txt`'s `[Daym1_]`-`[Daym6_]` sections (the Latin file's own unsuffixed `[Daym1]` etc.
just redirects to these via `@:` references), English parallel at the *unsuffixed*
`[Daym1]`-`[Daym6]` keys in the `English/` tree (no indirection there). Real wrinkle:
`psalter-distribution.yml`'s own `matins:` table isn't the historical per-weekday psalm set —
its own header documents a deliberate "editorial redistribution" (54 non-Sunday psalms
reclassified must-stay/could-move by length, then resliced into 6 fresh groups of 9 in
continuous psalm order), which breaks several of the source's own antiphon-sharing pairs across
weekday boundaries (e.g. Ps 43/44, originally Monday's own closing pair, now open Tuesday
instead). Resolved by keeping vu's existing 9-psalm weekday lists untouched and regrouping:
every psalm keeps its own original antiphon text, consecutive same-antiphon psalms stay grouped
whenever their pair survived the reslice intact, and a psalm whose original partner moved
elsewhere now stands alone under its own antiphon. Each weekday's single versicle is taken from
whichever source day contributes the majority of that weekday's 9 psalms, placed at the same
relative position it held there (verified group-by-group against both parallel source files —
see each weekday's own comment in the new data file for the exact reasoning).
New `data/hours/matins-ferial-antiphons.yml` (6 weekdays, 5-6 antiphon groups each, exactly one
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
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 —
`sundayPsalmNocturn`'s own versicle handling drops the R. line entirely (only pushes
`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.
`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,
correctly grouped, and exactly one versicle appears at the right point. Also fixed 3 stale
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
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)
and `tsc --noEmit` both pass.
## Known, deliberate simplifications (not bugs — working as designed) ## Known, deliberate simplifications (not bugs — working as designed)
- `getDayCollects`: each collect in a multi-collect day renders as its own - `getDayCollects`: each collect in a multi-collect day renders as its own
+249
View File
@@ -0,0 +1,249 @@
# Antiphons + versicles for the plain ferial weekday Matins nocturn (no
# Sunday, no Duplex+ override) — previously rendered with bare psalms and
# no antiphon at all (a genuine oversight, not a documented design choice
# like `ferialPsalmodyThreeNocturns`'s antiphon-less fallback).
#
# `psalter-distribution.yml`'s own `matins:` table is NOT the historical
# per-weekday psalm set — its own header documents a deliberate "editorial
# redistribution": the 54 non-Sunday psalms were reclassified must-stay/
# could-move by length, then resliced into 6 fresh groups of 9 in
# continuous psalm order, breaking several original antiphon-sharing pairs
# across weekday boundaries in the process (e.g. Ps 43/44 — originally
# Monday's own closing pair under "Eructávit" — now open Tuesday instead).
#
# So this file's own groups do NOT mirror the source's per-weekday
# grouping 1:1 — they're regrouped to fit vu's already-fixed 9-psalm list:
# every psalm here keeps its own original antiphon text (sourced from
# Divinum Officium's Monastic-labeled block, `Latin/Psalterium/Psalmi/
# Psalmi matutinum.txt`'s `[Daym1_]`-`[Daym6_]` sections, English parallel
# at `[Daym1]`-`[Daym6]` in the `English/` tree — the Latin file only
# carries the full per-weekday text under the `_`-suffixed key, redirecting
# its own unsuffixed `[Daym1]` etc. to it via `@:` references; the English
# file has no such indirection), consecutive same-antiphon psalms are kept
# grouped (this still happens whenever a pair survived the reslice
# intact), and any psalm whose original partner moved to a different
# weekday (or to a minor-hour slot, per `psalter-distribution.yml`'s own
# "could-move" psalms) now stands alone under its own antiphon instead.
#
# Versicle placement: each weekday draws its 9 psalms from 1-2 different
# original source days (see each weekday's own comment below for exactly
# which), but only one versicle fits per plain single-nocturn weekday —
# taken from whichever source day contributes the majority of that
# weekday's psalms, placed at the same relative position it held in that
# source day's own sequence (immediately after whichever group of this
# weekday's psalms was originally its next-door neighbor, once the
# psalms that moved out from between them are accounted for).
#
# Not live-verified against a running query (Monastic ferial ordinary time
# isn't independently re-derivable that way once psalter-distribution.yml's
# own redistribution has been applied) — sourced directly from the two
# parallel psalter files instead, cross-checked psalm-by-psalm against
# every entry in psalter-distribution.yml's own `matins:` table above.
monday:
# All 9 psalms are Daym1_'s own (Monastic Matins Feria II) — no reslicing
# across weekday boundaries here, so both antiphon grouping and the
# versicle sit exactly where Daym1_ itself has them.
groups:
- psalms: [{ number: 32 }, { number: 33 }]
antiphon:
la: "Rectos decet * collaudátio."
en: "Praise is comely * for the upright."
- psalms: [{ number: 34 }]
antiphon:
la: "Expúgna * impugnántes me."
en: "Fight against them * that fight against me."
- psalms: [{ number: 36, verses: "1-26" }, { number: 36, verses: "27-40" }]
antiphon:
la: "Revéla * Dómino viam meam."
en: "Commit my way * to the Lord."
versicle:
v: { la: "Dómine, in cælo misericórdia tua.", en: "O Lord, thy mercy is in heaven," }
r: { la: "Et véritas tua usque ad nubes.", en: "And thy truth reacheth even to the clouds." }
- psalms: [{ number: 37 }]
antiphon:
la: "Ne in ira tua * árguas me Dómine."
en: "Do not chastise me * in thy wrath."
- psalms: [{ number: 38 }, { number: 39 }]
antiphon:
la: "Ut non delínquam * in lingua mea."
en: "That I sin not * with my tongue."
- psalms: [{ number: 40 }]
antiphon:
la: "Sana Dómine * ánimam meam, quia peccávi tibi."
en: "Lord, heal my soul; * for I have sinned against thee."
tuesday:
# 43-44 are Monday's own tail (Daym1_); 47-58 are Daym2_'s own opening
# (6 of 9 psalms — the majority, hence Daym2_'s own versicle here); 59 is
# Wednesday's own opening (Daym3_). The versicle sits right after 49,
# exactly where Daym2_ itself has it (between the "49,51" and "52,53"
# groups — 51/52/53 all moved elsewhere in the reslice, but 49 and 54
# remain each other's nearest neighbor once they're gone).
groups:
- psalms: [{ number: 43 }, { number: 44 }]
antiphon:
la: "Eructávit * cor meum verbum bonum."
en: "Mine heart * is overflowing with a good matter."
- psalms: [{ number: 47 }, { number: 48 }]
antiphon:
la: "Magnus Dóminus * et laudábilis nimis."
en: "Great is the Lord, * and greatly to be praised."
- psalms: [{ number: 49 }]
antiphon:
la: "Deus deórum * Dóminus locútus est."
en: "The God of gods * the Lord hath spoken."
versicle:
v: { la: "Ímmola Deo sacrifícium laudis.", en: "Offer to God the sacrifice of praise." }
r: { la: "Et redde Altíssimo vota tua.", en: "And pay thy vows to the most High." }
- psalms: [{ number: 54 }, { number: 55 }]
antiphon:
la: "Inténde in me * et exáudi me."
en: "Be attentive to me * and hear me."
- psalms: [{ number: 58 }]
antiphon:
la: "Justa judicáte * fílii hóminum."
en: "Judge right things, * ye sons of men."
- psalms: [{ number: 59 }]
antiphon:
la: "Da nobis Dómine * auxílium de tribulatióne."
en: "Give us * help from trouble, O Lord."
wednesday:
# 65-72 are Daym3_'s own (7 of 9 — the majority); 73 is Thursday's own
# opening (Daym4). The versicle sits between the "67ab" and "68ab"
# groups, exactly where Daym3_ itself has it — nothing moved out from
# between those two groups in the reslice, so the position is unchanged.
groups:
- psalms: [{ number: 65 }]
antiphon:
la: "Benedícite, * gentes, Deum nostrum."
en: "O bless our God * ye people."
- psalms: [{ number: 67, verses: "2-19" }, { number: 67, verses: "20-36" }]
antiphon:
la: "In ecclésiis, * benedícite Dóminum."
en: "In the churches * bless ye the Lord."
versicle:
v: { la: "Benedícite, gentes, Deum nostrum.", en: "O bless our God, ye people." }
r: { la: "Et audítam fácite vocem laudis ejus.", en: "And make the voice of his praise to be heard." }
- psalms: [{ number: 68, verses: "2-16" }, { number: 68, verses: "17-37" }]
antiphon:
la: "Quǽrite Dóminum * et vivet ánima vestra."
en: "Seek ye the Lord * and your soul shall live."
- psalms: [{ number: 70 }]
antiphon:
la: "Dómine * Deus in adjutórium meum inténde."
en: "Make haste * O Lord God, to deliver me."
- psalms: [{ number: 71 }, { number: 72 }]
antiphon:
la: "Quam bonus * Israël Deus."
en: "How good is God * to Israel."
- psalms: [{ number: 73 }]
antiphon:
la: "Liberásti virgam * hereditátis tuæ."
en: "Thou hast redeemed the rod * of Thine inheritance."
thursday:
# All 9 psalms are Daym4's own — no reslicing across weekday boundaries
# here either, so grouping and versicle position are both unchanged from
# the source (the versicle sits between the "78" and "79,80" groups,
# exactly as Daym4 itself has it).
groups:
- psalms: [{ number: 76 }]
antiphon:
la: "Tu es Deus * qui facis mirabília."
en: "Thou art the God * That doest wonders."
- psalms: [{ number: 77, verses: "1-35" }, { number: 77, verses: "36-72" }]
antiphon:
la: "Inclináte * aurem vestram in verba oris mei."
en: "Incline your ears * to the words of My mouth."
- psalms: [{ number: 78 }]
antiphon:
la: "Propítius esto * peccátis nostris, Dómine."
en: "Forgive us our sins * O Lord."
versicle:
v: { la: "Deus, in sancto via tua.", en: "Thy way, O God, is in the holy place." }
r: { la: "Quis Deus magnus sicut Deus noster.", en: "Who is the great God like our God?" }
- psalms: [{ number: 79 }, { number: 80 }]
antiphon:
la: "Exsultáte Deo * adjutóri nostro."
en: "Sing aloud * unto God our strength."
- psalms: [{ number: 82 }]
antiphon:
la: "Tu solus * altíssimus super omnem terram."
en: "Thou alone * art the Most High over all the earth."
- psalms: [{ number: 83 }, { number: 84 }]
antiphon:
la: "Benedixísti * Dómine terram tuam."
en: "Thou hast blessed * thy land, O Lord."
friday:
# 85-95 are Daym5_'s own (5 of 9 — the majority: 85, 88ab, 93, 95 — note
# 93's original antiphon covered a 92/93 pair, 92 itself having moved
# elsewhere, so it's kept here on 93 alone; likewise 95's antiphon
# originally also covered 96, which the redistribution's own documented
# hand-adjustment moved to Saturday to keep both halves of Ps 103
# together there); 101-103 are Daym6_'s own opening (4 of 9). The
# versicle sits between the "93" and "95" groups, exactly where Daym5_
# itself has it (originally between "92/93" and "95/96" — 96 leaving for
# Saturday doesn't touch this boundary).
groups:
- psalms: [{ number: 85 }]
antiphon:
la: "Inclína Dómine, * aurem tuam, et exáudi me."
en: "Incline thy ear, O Lord, * and hear me."
- psalms: [{ number: 88, verses: "2-19" }, { number: 88, verses: "20-53" }]
antiphon:
la: "Benedíctus * Dóminus in ætérnum."
en: "Blessed be * the Lord for evermore."
- psalms: [{ number: 93 }]
antiphon:
la: "Exaltáre, * qui júdicas terram."
en: "Lift up thyself, * thou that judgest the earth."
versicle:
v: { la: "Intret orátio mea in conspéctu tuo, Dómine.", en: "Let my prayer come before Thee, O Lord." }
r: { la: "Inclína aurem tuam ad precem meam.", en: "Incline Thine ear unto my cry." }
- psalms: [{ number: 95 }]
antiphon:
la: "Cantáte Dómino, * et benedícite nómini ejus."
en: "Sing ye * to the Lord and bless his name."
- psalms: [{ number: 101 }, { number: 102 }]
antiphon:
la: "Clamor meus * ad te véniat, Deus."
en: "O God * let my cry come unto Thee."
- psalms: [{ number: 103, verses: "1-24" }, { number: 103, verses: "25-35" }]
antiphon:
la: "Bénedic, * ánima mea Dómino."
en: "Bless the Lord, * O my soul."
saturday:
# Ps 96 is Daym5_'s own tail (moved here by the redistribution's own
# documented hand-adjustment, swapped out of Friday to let both halves
# of Ps 103 land there instead); 104-108 are Daym6_'s own tail (8 of 9 —
# the majority, hence Daym6_'s own versicle here). The versicle sits
# between the "104ab" and "105ab" groups, exactly where Daym6_ itself
# has it — nothing moved out from between those two groups.
groups:
- psalms: [{ number: 96 }]
antiphon:
la: "Cantáte Dómino, * et benedícite nómini ejus."
en: "Sing ye * to the Lord and bless his name."
- psalms: [{ number: 104, verses: "1-22" }, { number: 104, verses: "23-45" }]
antiphon:
la: "Lætétur cor * quæréntium Dóminum."
en: "Let the heart of them rejoice, * that seek the Lord."
versicle:
v: { la: "Dómine, exáudi oratiónem meam.", en: "O Lord, hear my prayer." }
r: { la: "Et clamor meus ad te véniat.", en: "And let my cry come unto thee." }
- psalms: [{ number: 105, verses: "1-31" }, { number: 105, verses: "32-48" }]
antiphon:
la: "Vísita nos, * Dómine, in salutári tuo."
en: "Visit us * with Thy salvation, O Lord."
- psalms: [{ number: 106, verses: "1-24" }, { number: 106, verses: "25-43" }]
antiphon:
la: "De necessitátibus meis * éripe me, Dómine."
en: "From my necessities, deliver me, O Lord."
- psalms: [{ number: 107 }, { number: 108 }]
antiphon:
la: "Confitébor * Dómino nimis in ore meo."
en: "I will greatly praise * the Lord with my mouth."
+54 -1
View File
@@ -84,6 +84,8 @@ import { getNocturnReadings, type NocturnReading } from '../propers/nocturn-read
import { getOctaveReading } from '../propers/octave-readings'; import { getOctaveReading } from '../propers/octave-readings';
import { getMatinsPsalmodyOverride } from './matins-psalmody-overrides'; import { getMatinsPsalmodyOverride } from './matins-psalmody-overrides';
import matinsSundayAntiphonsData from '../data/hours/matins-sunday-antiphons.yml'; import matinsSundayAntiphonsData from '../data/hours/matins-sunday-antiphons.yml';
import matinsFerialAntiphonsData from '../data/hours/matins-ferial-antiphons.yml';
import type { Weekday } from '../calendar/types';
type BilingualText = Partial<Record<string, string>>; type BilingualText = Partial<Record<string, string>>;
interface SundayGroup { interface SundayGroup {
@@ -114,6 +116,17 @@ interface MatinsSundayAntiphons {
} }
const sundayAntiphons = matinsSundayAntiphonsData as unknown as MatinsSundayAntiphons; const sundayAntiphons = matinsSundayAntiphonsData as unknown as MatinsSundayAntiphons;
interface FerialGroup {
psalms: PsalmRef[];
antiphon: BilingualText;
versicle?: { v: BilingualText; r: BilingualText };
}
interface FerialNocturn {
groups: FerialGroup[];
}
type MatinsFerialAntiphons = Record<Exclude<Weekday, 'sunday'>, FerialNocturn>;
const ferialAntiphons = matinsFerialAntiphonsData as unknown as MatinsFerialAntiphons;
const NOCTURN_NUMERAL = ['I', 'II', 'III'] as const; const NOCTURN_NUMERAL = ['I', 'II', 'III'] as const;
type PsalmPart = Extract<ResolvedPart, { kind: 'psalm' }>; type PsalmPart = Extract<ResolvedPart, { kind: 'psalm' }>;
@@ -221,6 +234,46 @@ function ferialPsalmody(day: LiturgicalDay): ResolvedPart[] {
return psalmRefParts(getPsalmsFor('matins', day.weekday)); return psalmRefParts(getPsalmsFor('matins', day.weekday));
} }
/** The plain ferial weekday nocturn's real antiphoned psalmody (see
* data/hours/matins-ferial-antiphons.yml's own header for how each
* weekday's groups/versicle were reconciled against psalter-
* distribution.yml's own "editorial redistribution" of the historical
* per-weekday psalm set). Every psalm-group carries its own antiphon
* (incipit-or-full opening, same rank rule as Sunday's own nocturns), and
* exactly one group per weekday carries a versicle+responsory, rendered
* as one combined V./R. block (not just the V. line — see the sibling
* `sundayPsalmNocturn`'s own versicle handling, which drops the R. line
* entirely; not replicated here on purpose). */
function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] {
const nocturn = ferialAntiphons[day.weekday as Exclude<Weekday, 'sunday'>];
const isDouble = isDoubleOrHigher(resolveOfficeWinner(day));
const parts: ResolvedPart[] = [];
for (const group of nocturn.groups) {
const { incipit, full } = splitNamedAntiphon(verifiedText(group.antiphon));
const opening = isDouble ? full : incipit;
group.psalms.forEach((ref, i) => {
parts.push({
kind: 'psalm',
psalmNumber: ref.number,
verses: getPsalmVerses(ref.number, ref.verses).map((v): ResolvedVerse => ({ n: v.n, text: v.text, status: v.status })),
antiphon: i === 0 ? opening : undefined,
});
});
parts.push({ kind: 'antiphon', text: full });
if (group.versicle) {
const { v, r } = group.versicle;
parts.push({
kind: 'versicle',
text: verifiedText({
la: `V. ${v.la}\nR. ${r.la}`,
en: `V. ${v.en}\nR. ${r.en}`,
}),
});
}
}
return parts;
}
/** Fallback for a Duplex+ weekday feast with no matins-psalmody-overrides /** Fallback for a Duplex+ weekday feast with no matins-psalmody-overrides
* entry authored for its winner yet (most of them, until the bulk-content * entry authored for its winner yet (most of them, until the bulk-content
* pass — see hours/matins-psalmody-overrides.ts's own doc comment): the * pass — see hours/matins-psalmody-overrides.ts's own doc comment): the
@@ -401,7 +454,7 @@ export function resolveOrdo(date: string): ResolvedOrdo {
} }
parts.push({ kind: 'te-deum', text: resolveCommon('te-deum') }); parts.push({ kind: 'te-deum', text: resolveCommon('te-deum') });
} else { } else {
parts.push(...ferialPsalmody(day)); parts.push(...ferialAntiphonedNocturn(day));
parts.push(...(nocturn1Readings ?? [])); parts.push(...(nocturn1Readings ?? []));
parts.push({ kind: 'chapter', text: resolveCommon('matins-capitulum-ferial') }); parts.push({ kind: 'chapter', text: resolveCommon('matins-capitulum-ferial') });
} }
+30 -1
View File
@@ -43,6 +43,30 @@ describe('resolveOrdo("matins", ...) ferial (1-nocturn) branch', () => {
expect(ordo.parts.some((p) => p.kind === 'canticle')).toBe(false); expect(ordo.parts.some((p) => p.kind === 'canticle')).toBe(false);
}); });
it("gives every nocturn psalm its own antiphon and includes Tuesday's own versicle (data/hours/matins-ferial-antiphons.yml)", () => {
// The nocturn's own psalms start at index 4 (after versicle/Ps3/heading/Ps94).
const nocturnPsalms = ordo.parts.slice(4).filter((p) => p.kind === 'psalm') as {
psalmNumber: number;
antiphon?: { text: Record<string, string> };
}[];
expect(nocturnPsalms.map((p) => p.psalmNumber)).toEqual([43, 44, 47, 48, 49, 54, 55, 58, 59]);
// Groups: [43,44] [47,48] [49] [54,55] [58] [59] — antiphon only on
// each group's first psalm.
const withAntiphon = nocturnPsalms.filter((p) => p.antiphon !== undefined).map((p) => p.psalmNumber);
expect(withAntiphon).toEqual([43, 47, 49, 54, 58, 59]);
for (const p of nocturnPsalms) {
if (p.antiphon) {
expect(p.antiphon.text.la).toBeTruthy();
expect(p.antiphon.text.en).toBeTruthy();
}
}
const nocturnVersicles = ordo.parts.slice(4).filter((p) => p.kind === 'versicle');
expect(nocturnVersicles).toHaveLength(1);
const versicleText = (nocturnVersicles[0] as { text: { text: Record<string, string> } }).text.text;
expect(versicleText.la).toContain('Ímmola Deo sacrifícium laudis');
expect(versicleText.la).toContain('Et redde Altíssimo vota tua');
});
it("resolves the user's own bible-plan readings for the day, not the historical lectionary", () => { it("resolves the user's own bible-plan readings for the day, not the historical lectionary", () => {
const lessons = ordo.parts.filter((p) => p.kind === 'lesson'); const lessons = ordo.parts.filter((p) => p.kind === 'lesson');
// Plain 2 bible-plan readings on this particular clean ferial (no // Plain 2 bible-plan readings on this particular clean ferial (no
@@ -314,7 +338,12 @@ describe('resolveOrdo("matins", ...) Sacred Triduum', () => {
it.each([HOLY_THURSDAY, GOOD_FRIDAY, HOLY_SATURDAY])('drops the opening versicle/Ps 3/Invitatory/hymn on %s', (date) => { it.each([HOLY_THURSDAY, GOOD_FRIDAY, HOLY_SATURDAY])('drops the opening versicle/Ps 3/Invitatory/hymn on %s', (date) => {
const ordo = resolveOrdo('matins', date); const ordo = resolveOrdo('matins', date);
expect(ordo.parts.some((p) => p.kind === 'versicle')).toBe(false); // The opening versicle itself is gone -- Ps 3 (which it precedes) is
// gone too, and the nocturn's own psalmody starts the ordo straight
// off (see the "goes straight into Nocturn 1" test below). A versicle
// further in belongs to the ferial nocturn's own antiphoned psalmody
// (matins-ferial-antiphons.yml), not the dropped opening block.
expect(ordo.parts[0]?.kind).toBe('psalm');
expect(ordo.parts.some((p) => p.kind === 'psalm' && p.psalmNumber === 3)).toBe(false); expect(ordo.parts.some((p) => p.kind === 'psalm' && p.psalmNumber === 3)).toBe(false);
expect(ordo.parts.some((p) => p.kind === 'psalm' && p.psalmNumber === 94)).toBe(false); expect(ordo.parts.some((p) => p.kind === 'psalm' && p.psalmNumber === 94)).toBe(false);
expect(ordo.parts.some((p) => p.kind === 'section-heading' && p.label === 'Invitatory')).toBe(false); expect(ordo.parts.some((p) => p.kind === 'section-heading' && p.label === 'Invitatory')).toBe(false);