Files
vu/src/hours/compline.ts
T
will ec448bc35b
Deploy / deploy (push) Successful in 38s
compline: build the full hour, sharing Prime's resolver helpers
Ordo is Monastic 1617 as a base, with deliberate Tridentine 1906
additions: the "In manus tuas" short responsory, the Nunc Dimittis
with its "Salva nos" antiphon, the fuller Preces (Monastic's own is
shorter), and Lent's "Christe, qui lux es et dies" hymn replacement
(English translation marked draft, pending a real historical one).
Psalms 4, 90, 133 are fixed daily with real verified text, unlike
Prime's weekday-rotated psalms.

The closing Marian antiphon now picks Alma Redemptoris Mater / Ave
Regina Caelorum / Regina Caeli / Salve Regina by real season, using
the calendar work from the previous commit — except Ave Regina
Caelorum's own window (Candlemas through the day before Maundy
Thursday) doesn't align to any single season value, so that one case
checks the real date directly instead of going through the by-season
table.

resolve-common.ts extracts the resolve/status/doxology/antiphon
helpers Prime already had into something Compline can share; a few
propers files lose their "prime-" prefix now that both hours use them.
2026-08-10 05:27:40 -04:00

96 lines
4.1 KiB
TypeScript

import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart } from './types';
import type { LiturgicalDay } from '../calendar/types';
import { resolveDay } from '../calendar';
import { getPsalmVerses } from '../psalter';
import { getCommonProper } from '../propers';
import { getHymnDoxologyId } from './hymn-doxology';
import { getOpeningVersicleId } from './opening-versicle';
import { getMarianAntiphonId, getMarianAntiphonLabel } from './marian-antiphon';
import { isDoubleOrHigher } from './antiphon';
import { resolveCommon, appendDoxology, splitNamedAntiphon } 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.occurring, 'compline-hymn-doxology-per-annum'),
);
return [{ kind: 'hymn', text: appendDoxology(body, doxology) }];
}
case 'chapter':
case 'responsory':
case 'versicle':
case 'prayer':
return [{ kind: part.kind, text: resolveCommon(part.textRef.id) }];
case 'preces':
if (part.omitOnDouble && isDoubleOrHigher(day.occurring)) {
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.occurring)) }];
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(getCommonProper('nunc-dimittis-antiphon').text);
const opening = isDoubleOrHigher(day.occurring) ? 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':
throw new Error(`Compline's ordo doesn't support a '${part.kind}' part`);
}
}
export function resolveOrdo(date: string): ResolvedOrdo {
const day = resolveDay(date);
const parts = complineDefinition.parts.flatMap((part) => resolvePart(part, day));
return { hourId: 'compline', date, parts };
}