Files
vu/src/hours/compline.ts
T
will 9364185f70 Omit responsories' own closing Gloria Patri throughout Passiontide
A different, wider rule than the psalm/canticle Gloria (Triduum-only):
responsories drop their Gloria for the whole of Passiontide (Passion
Sunday through Holy Saturday), except on a Sancti feast's own day —
live-verified (St. Joseph, Duplex I, 2027-03-19, landing in Passiontide
that year, keeps it; an ordinary ferial day in the same window omits it,
at both Compline and Prime).

Every responsory's stored text differs, Gloria-bearing vs Gloria-free,
by exactly one fixed-wording line, so resolve-common.ts's new
resolveResponsory filters that line out rather than requiring separate
per-season files or a data migration — mechanism first, per the
project's own stated content-authoring order. Wired into the four hours
that have a real responsory (Compline, Prime, Lauds, Vespers); Lauds/
Vespers' existing hand-authored Passiontide-proper responsories were
already Gloria-free and are unaffected.
2026-08-19 06:38:21 -04:00

114 lines
4.9 KiB
TypeScript

import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart } from './types';
import type { LiturgicalDay } from '../calendar/types';
import { resolveEveningDay } from '../calendar/vespers';
import { getDayLabel } from '../calendar/day-label';
import { getPsalmVerses } from '../psalter';
import { getHymnDoxologyId } from './hymn-doxology';
import { getOpeningVersicleId } from './opening-versicle';
import { getMarianAntiphonId, getMarianAntiphonLabel } from './marian-antiphon';
import { isDoubleOrHigher } from './antiphon';
import { resolveCommon, appendDoxology, splitNamedAntiphon, resolveResponsory } from './resolve-common';
import complineDefinitionData from '../data/hours/compline.yml';
const complineDefinition = complineDefinitionData as HourDefinition;
// Real practice starts this at Lent proper (Ash Wednesday), not the
// preceding Septuagesima-tide — unlike the opening versicle's Alleluia
// suppression, which does start at Septuagesima (see
// data/hours/opening-by-season.yml). Two different season keys deliberately
// treated differently, not an oversight.
const LENTEN_HYMN_SEASONS = new Set(['lent', 'passiontide']);
function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
switch (part.kind) {
case 'hymn': {
// Lent replaces the whole hymn (not just its doxology) with "Christe,
// qui lux es et dies" — self-contained, so no appendDoxology here.
// See compline-hymn-lenten.yml for real caveats about how confident
// to be that this actually triggers in live practice.
if (LENTEN_HYMN_SEASONS.has(day.season)) {
return [{ kind: 'hymn', text: resolveCommon('compline-hymn-lenten') }];
}
const body = resolveCommon(part.textRef.id);
const doxology = resolveCommon(
getHymnDoxologyId(day.season, day.winner, 'compline-hymn-doxology-per-annum'),
);
return [{ kind: 'hymn', text: appendDoxology(body, doxology) }];
}
case 'chapter':
case 'versicle':
case 'prayer':
return [{ kind: part.kind, text: resolveCommon(part.textRef.id) }];
case 'responsory':
return [{ kind: 'responsory', text: resolveResponsory(resolveCommon(part.textRef.id), day) }];
case 'preces':
if (part.omitOnDouble && isDoubleOrHigher(day.winner)) {
return [];
}
return [{ kind: 'preces', text: resolveCommon(part.textRef.id), label: part.label }];
case 'lesson':
return [{ kind: 'lesson', text: resolveCommon(part.textRef.id), label: part.label }];
case 'opening-versicle':
return [{ kind: 'versicle', text: resolveCommon(getOpeningVersicleId(day.season, day.winner)) }];
case 'psalm':
return [
{
kind: 'psalm',
psalmNumber: part.psalmNumber,
verses: getPsalmVerses(part.psalmNumber, part.verses).map((v) => ({
n: v.n,
text: v.text,
status: v.status,
})),
},
];
case 'nunc-dimittis': {
const { incipit, full } = splitNamedAntiphon(resolveCommon('nunc-dimittis-antiphon'));
const opening = isDoubleOrHigher(day.winner) ? full : incipit;
return [
{ kind: 'canticle', canticleId: 'nunc-dimittis', text: resolveCommon('nunc-dimittis'), antiphon: opening },
{ kind: 'antiphon', text: full },
];
}
case 'marian-antiphon': {
const id = getMarianAntiphonId(day);
return [{ kind: 'preces', text: resolveCommon(id), label: getMarianAntiphonLabel(id) }];
}
case 'canticle':
return [{ kind: 'canticle', canticleId: part.canticleId, text: resolveCommon(part.textRef.id) }];
// Not used by Compline — Prime-only, or milestone-4-only. Throwing
// rather than silently dropping: if one of these ever shows up in
// compline.yml, that's a mistake worth surfacing loudly, not hiding.
case 'martyrology':
case 'rule-reading':
case 'creed':
case 'closing-antiphon':
case 'by-day-kind':
case 'variable':
case 'day-collect':
case 'lauds-psalmody':
case 'lauds-office':
case 'benedictus':
case 'suffrages':
case 'lauds-preces':
case 'day-collects':
case 'vespers-psalmody':
case 'vespers-office':
case 'magnificat':
case 'vespers-preces':
throw new Error(`Compline's ordo doesn't support a '${part.kind}' part`);
}
}
export function resolveOrdo(date: string): ResolvedOrdo {
// Every season/occurring-dependent part below (hymn, doxology, Preces
// omitOnDouble, opening versicle, Nunc Dimittis rank, Marian antiphon) is
// an evening-hour concern, so the whole ordo resolves off whichever day's
// identity actually governs tonight — see calendar/vespers.ts. Nothing
// here is date-literal the way Prime's Martyrology is, so there's no
// need to keep a separate "real" day around.
const day = resolveEveningDay(date);
const parts = complineDefinition.parts.flatMap((part) => resolvePart(part, day));
return { hourId: 'compline', date, parts, dayLabel: getDayLabel(day) };
}