Finish the last 5 vigils: weekday-keyed commemoration antiphons
Deploy / deploy (push) Successful in 1m32s

Corrected an earlier "no source exists" claim by live-querying real
commemorated instances of the Vigils of St. Lawrence and the Assumption
across six different weekdays. Their source files' own [Rule] block
("Versum Feria") turns out to be a real, sourced mechanism, not an
absence: a mere commemoration doesn't reuse the vigil's own antiphon at
all -- it borrows the plain ferial day's antiphon/versicle instead, and
that antiphon quotes the Benedictus canticle in weekday sequence (same
shape vespers-magnificat-antiphons.yml already uses for the Magnificat).
The versicle is a single fixed text. Sunday has no entry: a Vigil
transfers off an ordinary Sunday rather than ever reaching a
commemoration there, confirmed live as well as already true in this
app's own precedence rules.

sanctoralCommemorationPart now takes weekday and resolves this
dynamically for common-of-a-vigil saints, rather than needing a static
per-saint file. All 218 saints with an authored collect now have a real
commemoration antiphon.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
This commit is contained in:
2026-08-30 18:12:12 -04:00
parent b5f16ceb23
commit 04a8686551
4 changed files with 172 additions and 9 deletions
+11 -6
View File
@@ -64,12 +64,17 @@ across every hour and content type in this app.
Holy Cross/Most Precious Blood/Transfiguration, given their own real `[Ant 2]`/`[Versum 2]`
straight from their Sancti files). **213 of ~218 real saints now covered**
(`tests/propers/commemoration-sweep.test.ts`, 213 cases); every entry not independently
live-verified is marked `status: draft`. Only 5 remain, genuinely open: the Vigils
(`common-of-a-vigil` — All Saints, Ss. Simon and Jude, St. John the Baptist, St. Lawrence,
the Assumption) — no dedicated "Commune Vigiliarum" antiphon set exists in the reference
engine at all (only `C1v`, Vigil of Apostles specifically, itself empty/inherited), and
`FeastClass: 'vigil'` is weak enough that these rarely reach the commemoration code path in
the first place — real, but low-priority.
live-verified is marked `status: draft`. **DONE, full 218/218**: the remaining 5 Vigils
(`common-of-a-vigil`) turned out not to need a static template at all — pushed back on the
earlier "no source exists" claim by live-querying real commemorated instances directly, which
found their own source file's `[Rule]` ("Versum Feria") is a real, sourced mechanism: the
antiphon quotes the Benedictus canticle in weekday sequence (confirmed against 6 real
weekdays, same shape `vespers-magnificat-antiphons.yml` already uses for the Magnificat), the
versicle is a single fixed text, and Sunday genuinely has no entry (a Vigil transfers off an
ordinary Sunday rather than ever reaching a commemoration there — confirmed live too).
Implemented as a small weekday-aware code path in `sanctoralCommemorationPart` (now takes
`weekday`) rather than a per-saint file — see `data/hours/lauds-vigil-commemoration-
antiphons.yml` and `tests/hours/vigil-commemoration.test.ts`.
- Vespers Magnificat antiphon: after the 2026-08-30 sweep plus the same-day O-Antiphon
follow-up (see "O Antiphons (Dec 17-23) added" below), only two narrow gaps remain —
Advent 4's own Sunday landing on Dec 24 specifically (outside the O-Antiphon range), and
@@ -0,0 +1,63 @@
# A mere commemoration of a Vigil (`common: common-of-a-vigil` — St.
# Lawrence's, the Assumption's, All Saints', Ss. Simon and Jude's, St.
# John the Baptist's) does NOT reuse that vigil's own antiphon (the one
# used when it wins outright, e.g. vigil-of-st-lawrence-antiphon.yml) —
# its own source file's [Rule] block says "Versum Feria" (borrow the
# plain ferial day's own versicle/antiphon instead), confirmed live
# (Monastic Tridentinum 1617): the Vigil of St. Lawrence's commemoration
# on a real Monday (2027-08-09) and the Vigil of the Assumption's on six
# different real weekdays (2026 Fri/2027 Sat/2028 Mon/2029 Tue/2030
# Wed/2031 Thu) all gave the exact same result for their weekday — the
# antiphon quotes the Benedictus canticle's own text in weekday sequence
# (same "quote the framing canticle in order through the week" pattern
# vespers-magnificat-antiphons.yml already uses for Vespers), same text
# regardless of *which* vigil. Sunday has no entry: a Vigil (`FeastClass:
# 'vigil'`, weaker than Semiduplex) never actually reaches a commemoration
# on an ordinary Sunday in this app's own precedence rules either — it
# transfers instead (calendar/commemorations.ts's `ordinary-sunday`
# branch) — confirmed live too (2033-08-14, a real Sunday: the Vigil of
# the Assumption doesn't appear at all, only the Lawrence-octave day is
# commemorated).
#
# English: the reference engine's own English files have no translation
# for this specific commemoration antiphon at all (checked across every
# weekday) -- these are direct translations, informed by the Benedictus
# canticle's own already-verified English (propers/common/benedictus.yml)
# for vocabulary consistency, but not independently sourced -- `status`
# marks them `draft` for that reason.
monday:
antiphon:
la: "Benedíctus Dóminus, Deus Israël."
en: "Blessed be the Lord God of Israel."
status: { la: verified, en: draft }
tuesday:
antiphon:
la: "Eréxit nobis * Dóminus cornu salútis in domo David púeri sui."
en: "The Lord hath raised up for us * a horn of salvation in the house of David his servant."
status: { la: verified, en: draft }
wednesday:
antiphon:
la: "De manu ómnium qui odérunt nos, líbera nos Dómine."
en: "From the hand of all that hate us, deliver us, O Lord."
status: { la: verified, en: draft }
thursday:
antiphon:
la: "In sanctitáte serviámus Dómino, et liberábit nos ab inimícis nostris."
en: "Let us serve the Lord in holiness, and he will deliver us from our enemies."
status: { la: verified, en: draft }
friday:
antiphon:
la: "Per víscera misericórdiæ Dei nostri visitávit nos Óriens ex alto."
en: "Through the tender mercy of our God, the Dayspring from on high hath visited us."
status: { la: verified, en: draft }
saturday:
antiphon:
la: "Illumináre, Dómine, his qui in ténebris sedent: et dírige pedes nostros in viam pacis, Deus Israël."
en: "Give light, O Lord, to them that sit in darkness: and direct our feet into the way of peace, O God of Israel."
status: { la: verified, en: draft }
+55 -3
View File
@@ -10,6 +10,7 @@ import { splitAntiphon, isDoubleOrHigher } from './antiphon';
import { getLaudsSaintOverride } from './lauds-psalmody-overrides';
import vespersMagnificatAntiphonsData from '../data/hours/vespers-magnificat-antiphons.yml';
import vespersOAntiphonsData from '../data/hours/vespers-o-antiphons.yml';
import laudsVigilCommemorationAntiphonsData from '../data/hours/lauds-vigil-commemoration-antiphons.yml';
type BilingualText = Partial<Record<string, string>>;
type TranslationStatus = 'verified' | 'draft' | 'missing';
@@ -26,6 +27,10 @@ const vespersMagnificatAntiphons = vespersMagnificatAntiphonsData as Partial<
// these seven can't live in a per-Advent-week temporal-proper file the
// way every other week's Magnificat antiphon does.
const vespersOAntiphons = vespersOAntiphonsData as Partial<Record<string, MagnificatWeekdayDefault>>;
// Monday-Saturday only — see lauds-vigil-commemoration-antiphons.yml's own
// header for why Sunday has no entry (a Vigil never reaches a
// commemoration on an ordinary Sunday in the first place).
const vigilCommemorationAntiphons = laudsVigilCommemorationAntiphonsData as Partial<Record<Weekday, MagnificatWeekdayDefault>>;
function toResolvedText(proper: ProperText): ResolvedText {
return { text: proper.text, status: proper.status, citation: proper.citation };
@@ -597,6 +602,41 @@ export function getDayCollect(day: LiturgicalDay): ResolvedText {
return { text: {}, status: { la: 'missing', en: 'missing' } };
}
/** A Vigil (`common: common-of-a-vigil`) doesn't reuse its own antiphon
* when merely commemorated — its own source file's `[Rule]` says "Versum
* Feria" (borrow the plain ferial day's own antiphon/versicle instead),
* confirmed live across six different real weekdays (see
* data/hours/lauds-vigil-commemoration-antiphons.yml's own header for the
* dates checked) — the antiphon quotes the Benedictus canticle in weekday
* sequence (same pattern vespers-magnificat-antiphons.yml already uses
* for the Magnificat), same text regardless of *which* vigil, and the
* versicle is a single fixed text (`lauds-versicle-monday.yml`'s own,
* confirmed identical even on a live Saturday, so genuinely constant, not
* itself weekday-keyed despite superficially matching most weekdays'
* plain ferial versicle). Sunday has no entry — a Vigil never reaches a
* commemoration on an ordinary Sunday in this app's own precedence rules
* either (calendar/commemorations.ts's `ordinary-sunday` branch
* transfers it instead), confirmed live too. */
function vigilCommemorationAntiphon(weekday: Weekday): ResolvedText | undefined {
const entry = vigilCommemorationAntiphons[weekday];
return entry ? (entry.status ? { text: entry.antiphon, status: entry.status } : verifiedText(entry.antiphon)) : undefined;
}
function combineCommemorationBundle(antiphon: ResolvedText, versicle: ResolvedText, collect: ResolvedText): ResolvedText {
const text: Partial<Record<string, string>> = {};
const status: Partial<Record<string, 'verified' | 'draft' | 'missing'>> = {};
for (const lang of new Set([...Object.keys(antiphon.text), ...Object.keys(versicle.text), ...Object.keys(collect.text)])) {
const parts = [antiphon, versicle, collect].map((t) => t.text[lang]).filter((t): t is string => !!t);
if (parts.length === 0) continue;
text[lang] = `Ant. ${parts[0] ?? ''}\n${parts[1] ?? ''}\n${parts[2] ?? ''}`.trim();
const statuses = [antiphon.status[lang], versicle.status[lang], collect.status[lang]].filter(
(s): s is 'verified' | 'draft' | 'missing' => !!s,
);
status[lang] = statuses.reduce((worst, s) => (STATUS_RANK[s] > STATUS_RANK[worst] ? s : worst), 'verified' as const);
}
return { text, status };
}
/** A sanctoral commemoration's own rendering: the fuller Ant+V/R+collect
* bundle (`${propers}-commemoration`, e.g. st-clare-commemoration.yml)
* when authored — live-verified this is the real shape a commemoration
@@ -605,8 +645,13 @@ export function getDayCollect(day: LiturgicalDay): ResolvedText {
* replaces) when only that's been authored, and to an honestly labeled
* "missing" block when neither has. Always labeled ("Commemoration of St.
* X") so an unauthored one reads as "this saint's commemoration isn't
* written up yet," not as a mystery blank prayer. */
function sanctoralCommemorationPart(commemoration: Extract<Commemoration, { kind: 'sanctoral' }>): ResolvedPart {
* written up yet," not as a mystery blank prayer. A Vigil is a special
* case within this same fallback chain: see vigilCommemorationAntiphon's
* own doc comment. */
function sanctoralCommemorationPart(
commemoration: Extract<Commemoration, { kind: 'sanctoral' }>,
weekday: Weekday,
): ResolvedPart {
const label = `Commemoration of ${commemoration.name}`;
const saint = getSaintRecord(commemoration.id);
if (saint?.propers) {
@@ -616,6 +661,13 @@ function sanctoralCommemorationPart(commemoration: Extract<Commemoration, { kind
}
const properCollect = resolveCommon(`${saint.propers}-collect`);
if (properCollect.status.la !== 'missing' || properCollect.status.en !== 'missing') {
if (saint.common === 'common-of-a-vigil') {
const antiphon = vigilCommemorationAntiphon(weekday);
if (antiphon) {
const versicle = resolveCommon('lauds-versicle-monday');
return { kind: 'preces', text: combineCommemorationBundle(antiphon, versicle, properCollect), label };
}
}
return { kind: 'preces', text: properCollect, label };
}
}
@@ -693,7 +745,7 @@ export function getDayCollects(day: LiturgicalDay): ResolvedPart[] {
if (commemoration.kind === 'temporal') {
parts.push({ kind: 'prayer', text: toResolvedText(getTemporalProper(`${commemoration.id}-collect`)) });
} else if (commemoration.kind === 'sanctoral') {
parts.push(sanctoralCommemorationPart(commemoration));
parts.push(sanctoralCommemorationPart(commemoration, day.weekday));
} else if (commemoration.kind === 'octave' && commemoration.id !== substitutedOctaveId) {
parts.push(octaveCommemorationPart(commemoration));
}
+43
View File
@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest';
import { resolveOrdo } from '../../src/hours';
// A Vigil's mere commemoration (as opposed to winning outright, which
// uses its own real antiphon — see data/propers/common/vigil-of-st-
// lawrence-antiphon.yml etc.) doesn't reuse that antiphon at all: its own
// source file's [Rule] says "Versum Feria" — borrow the plain ferial
// day's own antiphon/versicle instead. Live-verified (Monastic
// Tridentinum 1617) across six real weekdays: the antiphon quotes the
// Benedictus canticle in weekday sequence (same shape as Vespers' own
// Magnificat-antiphon weekday default), the versicle is a single fixed
// text. See hours/resolve-common.ts's vigilCommemorationAntiphon and
// data/hours/lauds-vigil-commemoration-antiphons.yml's own header for the
// exact dates checked.
describe('a Vigil\'s mere commemoration (not winning outright)', () => {
it('2027-08-09 (Monday): Vigil of St. Lawrence commemorated under Ss. Cyriacus etc., Monday\'s own Benedictus-verse antiphon', () => {
const lauds = resolveOrdo('lauds', '2027-08-09');
const commem = lauds.parts.find((p) => p.kind === 'preces' && p.label?.includes('Vigil of St. Lawrence'));
expect(commem?.kind === 'preces' ? commem.text.text.la : undefined).toContain('Benedíctus Dóminus, Deus Israël.');
expect(commem?.kind === 'preces' ? commem.text.text.la : undefined).toContain('Repléti sumus mane misericórdia tua');
expect(commem?.kind === 'preces' ? commem.text.text.la : undefined).toContain('Adésto, Dómine'); // his own real collect, unchanged
expect(commem?.kind === 'preces' ? commem.text.status.la : undefined).toBe('verified');
});
it('2029-08-14 (Tuesday) vs. 2030-08-14 (Wednesday): the same Vigil of the Assumption gets a different antiphon on a different weekday, same fixed versicle', () => {
const tuesday = resolveOrdo('lauds', '2029-08-14');
const wednesday = resolveOrdo('lauds', '2030-08-14');
const tueCommem = tuesday.parts.find((p) => p.kind === 'preces' && p.label?.includes('Vigil of the Assumption'));
const wedCommem = wednesday.parts.find((p) => p.kind === 'preces' && p.label?.includes('Vigil of the Assumption'));
const tueText = tueCommem?.kind === 'preces' ? tueCommem.text.text.la : undefined;
const wedText = wedCommem?.kind === 'preces' ? wedCommem.text.text.la : undefined;
expect(tueText).toContain('Eréxit nobis');
expect(wedText).toContain('De manu ómnium qui odérunt nos');
expect(tueText).toContain('Repléti sumus mane misericórdia tua');
expect(wedText).toContain('Repléti sumus mane misericórdia tua');
});
it('2033-08-14 (a real Sunday): the Vigil is never even commemorated — a Vigil transfers off an ordinary Sunday, matching the live reference exactly', () => {
const lauds = resolveOrdo('lauds', '2033-08-14');
const commem = lauds.parts.find((p) => p.kind === 'preces' && p.label?.includes('Vigil of the Assumption'));
expect(commem).toBeUndefined();
});
});