Add the O Antiphons (Dec 17-23) as an unconditional Vespers Magnificat override
Deploy / deploy (push) Successful in 1m27s

advent-4's own remaining Magnificat-antiphon gap turned out to be a
mechanism problem, not missing content: Dec 17-23 always carries one of
the seven "O Antiphons" at Vespers, keyed to the calendar date itself,
not to whichever Advent week (advent-3 or advent-4, varies by year)
happens to govern that date -- a single per-week file was structurally
the wrong shape.

Added vespers-o-antiphons.yml (MM-DD keyed) and a new lookup tier in
getMagnificatAntiphon, checked unconditionally ahead of both the
per-week temporal file and a real sanctoral winner's own antiphon --
direct instruction: the O Antiphon wins this one text slot on these
seven dates regardless of who otherwise wins the day (e.g. St. Thomas,
Dec 21, still keeps every other part of his own office). Two of the
seven (O Clavis David/O Oriens, Dec 20-21) couldn't be live-verified --
every reference-engine track routes those two dates through St.
Thomas's Vigil/feast instead, which this app's own vigil-of-st-thomas.yml
already documents diverging from -- so they're marked draft rather than
verified.

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 07:04:30 -04:00
parent 66bd970fed
commit b3a7176cb8
5 changed files with 324 additions and 17 deletions
+113
View File
@@ -48,6 +48,12 @@ across every hour and content type in this app.
- The bulk Vulgate (Latin) + Douay-Rheims (English) scripture import into - The bulk Vulgate (Latin) + Douay-Rheims (English) scripture import into
`src/data/scripture/` — long-term project, explicitly deferred; Matins scripture `src/data/scripture/` — long-term project, explicitly deferred; Matins scripture
citations resolve to `missing` text until it lands. citations resolve to `missing` text until it lands.
- 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
`marian-saturday` (moot in practice — its own Vespers is never actually reachable, a
following Sunday always claims First Vespers first). Neither resolves from the static
reference files alone; would need a live-CGI query.
- Lauds/Vespers duplex-majus+ Common-category psalmody: 10 of 24 `SaintRecord.common` - Lauds/Vespers duplex-majus+ Common-category psalmody: 10 of 24 `SaintRecord.common`
categories authored (covering 19 ids via aliases) for Vespers, 9 of 24 (18 ids) for Lauds — categories authored (covering 19 ids via aliases) for Vespers, 9 of 24 (18 ids) for Lauds —
see "Lauds/Vespers duplex-majus+ Common-category psalmody" below and its follow-up entry. see "Lauds/Vespers duplex-majus+ Common-category psalmody" below and its follow-up entry.
@@ -4096,3 +4102,110 @@ rather than a drop. Regression test added (`tests/hours/compline.test.ts`, 2026-
Assumption, Duplex I. classis) confirming the short-form text (Kyrie/Pater/"Dómine, exáudi") is Assumption, Duplex I. classis) confirming the short-form text (Kyrie/Pater/"Dómine, exáudi") is
present and the fuller exchanges ("Benedíctus es, Dómine.../Miserére nostri...") are absent. present and the fuller exchanges ("Benedíctus es, Dómine.../Miserére nostri...") are absent.
`npm test` (543 passed), `tsc --noEmit`, and `npm run build` all pass. `npm test` (543 passed), `tsc --noEmit`, and `npm run build` all pass.
### Vespers Magnificat antiphon — whole-calendar temporal-cycle gap closed, 52/54 (2026-08-30)
User flagged 2026-08-29's Vespers was missing its Magnificat antiphon. Root cause turned out to
be systematic, not date-specific: `getMagnificatAntiphon` (`hours/resolve-common.ts`) looks up
`${winner.id}-magnificat-antiphon` for a temporal winner, but of the 57 temporal ids with an
authored Lauds `-benedictus-antiphon.yml`, only 3 (`holy-name-of-jesus`, `immaculate-heart-of-mary`,
`circumcision`) had a matching Vespers `-magnificat-antiphon.yml` — every ordinary Sunday/named-feast
in the temporal cycle (all 24 `post-pentecost-NN` weeks, Advent, post-Epiphany, pre-Lent, Lent,
Passion/Palm Sunday, the whole Easter season, Pentecost, Christmas-octave Sunday, Christ the King)
had its Lauds antiphon but not its Vespers one, apparently missed whenever the Lauds sweep pulled
`[Ant 2]` from the reference engine's Tempora files but never followed up with `[Ant 3]`.
Sourced from the same static reference-engine files already used for the Benedictus sweep
(`../divinum-officium-reference/web/www/horas/{Latin,English}/Tempora/*.txt`), same occasion/date
as each id's own `-benedictus-antiphon.yml` sibling (its header comment already records the clean
verified date) — no live CGI needed, the Magnificat antiphon is baked into each static file's own
`[Ant 3]` block (sometimes an alias, e.g. `@:Ant 3_` or `@:Ant 1`, always resolved to its real text
before use, and always the *default*/Tridentine variant, never a `(rubrica cisterciensis...)` one).
`post-pentecost-01`..`23` map straight to `Tempora/PentNN-0.txt`; `post-pentecost-24` (the fixed
last Sunday) does *not* map to `Pent24-0` — cross-checked against its own already-authored
Benedictus antiphon and confirmed it actually reuses the leftover `post-epiphany-6` formulary
(`Tempora/Epi6-0.txt`), the classic "Sundays remaining over are supplied by the omitted
post-Epiphany Sundays" rubric, already implicit in this app's own resumed-Epiphany-Sunday overflow
logic. `post-epiphany-1` doesn't map to `Epi1-0.txt` (that file is Holy Family Sunday content in
the reference engine, a title mismatch against the already-authored Benedictus text) but to
`Epi1-0a.txt` ("Dominica infra Octavam Epiphaniæ"). `christ-the-king` (not a Tempora entry at all,
a 1925 addition) sourced from `Sancti/10-DU.txt` instead, matching its own Benedictus antiphon's
already-verified occasion.
**Two ids skipped, not fabricated**: `advent-4` (`Tempora/Adv4-0.txt` has no standalone `[Ant 3]`
at all — only `[Ant Vespera 3] @:Ant Laudes`, aliasing the whole 5-antiphon Lauds set rather than
one text, and which single entry the live engine actually picks isn't determinable from the static
file alone) and `marian-saturday` (the votive "Sanctæ Mariæ Sabbato" office's Magnificat antiphon
isn't in any static Tempora/Sancti/Commune file found — likely only resolvable via a live CGI
query, same as how its own Benedictus antiphon was originally sourced).
### O Antiphons (Dec 17-23) added — advent-4's own gap effectively closed (2026-08-30)
Follow-up the same day, prompted by a user report that 2026-08-29's own Vespers was missing its
Magnificat antiphon entirely for an unrelated reason (see the day-label winner-first fix earlier
this date) that turned out to expose this whole-calendar gap. Root cause of `advent-4`'s own
skip above, once actually investigated live (divinumofficium.com, Monastic Tridentinum 1617):
Dec 17-23 always carries one of the seven "O Antiphons" at Vespers, keyed to the *calendar date*
itself, not to whichever Advent week (`advent-3` or `advent-4`, depending on the year) happens to
govern that date — a single per-week static file was structurally the wrong shape for this from
the start, not just missing content.
Added `data/hours/vespers-o-antiphons.yml` (7 entries, `MM-DD` keyed) and a new lookup tier in
`hours/resolve-common.ts`'s `getMagnificatAntiphon`. 5 of the 7 (Dec 17/18/19/22/23)
live-verified directly. The remaining two — "O Clavis David" (Dec 20) and "O Oriens" (Dec 21) —
could not be: every version/track on the live engine (including plain Tridentine 1570) routes
those two dates through St. Thomas's Vigil/feast instead of the ferial Proper of the Time,
apparently a real modeling choice of that engine (a Vigil of an Apostle actually displacing the
ferial Magnificat antiphon), not an app quirk — and this app's own `vigil-of-st-thomas.yml`
already documents a deliberate divergence from exactly that engine behavior on this date (the
Vigil transfers away entirely here, per the ordinary privileged-feria/vigil rule). Marked `draft`
rather than `verified`: real, standard, universally-fixed O-Antiphon wording (unchanged across
every edition of the Roman/Monastic Breviary), just not confirmed against this app's own primary
live reference for lack of a clean query path — see the data file's own header.
**Follow-up the same day, direct user instruction**: the O Antiphon lookup was originally scoped
to only apply when the day's winner was still plain-temporal (i.e. it deferred to a real
sanctoral winner, same as every other tier in `getMagnificatAntiphon`). The user explicitly
wanted the O Antiphons to actually surface even against a real winning saint — St. Thomas (Dec
21, Duplex II. classis) otherwise wins that day outright with no Magnificat antiphon of his own
authored yet (a separate, pre-existing gap — no sanctoral saint has a
`${propers}-magnificat-antiphon.yml` at all), so "O Oriens" was structurally unreachable on its
own real date. Considered and rejected: lowering Thomas's rank so a ferial default would win Dec
20/21 instead — factually wrong (he really is Duplex II Class pre-1955) and would strip him of
his *entire* office (chapter/hymn/collect/Lauds antiphon too), not just Vespers' Magnificat.
Instead, moved the O-Antiphon lookup to the very top of `getMagnificatAntiphon`, checked
unconditionally before either the temporal or sanctoral branch — the O Antiphon now wins this one
specific text slot on Dec 17-23 full stop, regardless of who otherwise wins the day; every other
part of the office is untouched. Deliberate, explicit app-level design choice (not a
reconstruction of the reference engine's own Dec 20/21 behavior, which shows Thomas's own
antiphon there instead) — see the data file's own header for the reasoning, including why a
saint's own real Magnificat antiphon is still worth authoring later even knowing it would be
permanently unreachable at Vespers on whichever of these seven dates that saint occupies, under
*this* calendar's own construction: the user has a future user-editable calendar in mind, where a
differently-constructed calendar's own precedence might surface it. `tests/hours/vespers.test.ts`'s
Thomas-precedence case now asserts the opposite of its original intent (the O Antiphon wins, his
other office parts don't).
Net effect: `advent-4`'s practical gap is closed for every date this app's calendar can actually
reach — Dec 17-23 unconditionally now, including Dec 21 itself. One narrow residual sub-case
remains, not touched by this fix: if Advent 4's own *Sunday* itself lands on Dec 24 (the latest
possible date, outside the O-Antiphon range and excluded from the classic weekday-default
fallback since Sunday has none), it still resolves to `missing` — genuinely no source for that
one specific Sunday found yet. `marian-saturday` is unchanged by this pass (still skipped, per
above) — and turns out to be moot for Vespers specifically anyway: its own evening is never
actually reachable through `resolveOrdo`, since a following Sunday always claims First Vespers
over the preceding Saturday regardless of the Sunday's own rank (`calendar/vespers.ts`'s
`hasFirstVespers`).
New tests: `tests/hours/vespers.test.ts`'s "O Antiphons (Dec 17-23)" block (the Dec 19 case
end-to-end, plus the Thomas-precedence case); `tests/hours/resolve-common.test.ts`'s new
`getMagnificatAntiphon — classic weekday-default fallback` block replaces the old bare-ferial
`resolveOrdo` test with two synthetic-day unit tests instead, since no live date is left in this
app's calendar that still reaches that fallback tier through the real pipeline at all (every
other temporal id now has its own real content, per this whole sweep). `npm test` (556 tests)
and `tsc --noEmit` both pass; live-verified on the dev server (2026-08-29 and 2026-12-19).
52 new `src/data/propers/temporal/*-magnificat-antiphon.yml` files. `npm test` (553 passed) and
`tsc --noEmit` both pass; spot-checked live in the dev server that 2026-08-29's Vespers now shows
the Magnificat antiphon ("Quǽrite primum * regnum Dei et justítiam ejus...", `post-pentecost-14`'s
own).
+86
View File
@@ -0,0 +1,86 @@
# The seven "O Antiphons" (Dec 17-23): the classic pre-Christmas Vespers
# Magnificat antiphons, keyed by calendar date (`MM-DD`) rather than by
# any Advent-week temporal id -- unlike every other week's proper
# Magnificat antiphon (data/propers/temporal/advent-N-magnificat-
# antiphon.yml, one file per Advent week), these seven don't belong to a
# single Advent week at all, since Dec 17-23 can fall within either
# Advent's 3rd or 4th week depending on the year (Advent's own governing
# Sunday moves, see calendar/temporal-id.ts).
#
# See hours/resolve-common.ts's getMagnificatAntiphon for the date-keyed
# lookup this feeds -- checked *unconditionally*, ahead of both the
# per-week temporal file and a real sanctoral winner's own antiphon.
# Direct user instruction (2026-08-30): the O Antiphon always wins this
# one specific text slot on these seven dates, full stop, regardless of
# who otherwise wins the day -- e.g. St. Thomas, Duplex II. classis,
# Dec 21, still wins everything else about that day (chapter, hymn,
# collect, Lauds' own Benedictus antiphon), just not the Magnificat
# antiphon. This is a deliberate app-level design choice, not a
# reconstruction of the reference engine's own behavior (its live query
# shows Thomas's own antiphon on Dec 20/21 instead, see below) -- and it
# means a saint's own real Magnificat antiphon, once authored, is
# permanently unreachable at Vespers on whichever of these seven dates
# that saint happens to occupy, under *this* calendar's own construction.
# Kept on file anyway (worth having for a future differently-constructed,
# e.g. user-editable, calendar) rather than treated as pointless to
# author.
#
# 12-17/18/19/22/23 live-verified against Divinum Officium (Monastic
# Tridentinum 1617), Vespers live query, 2027-12-17/18/19/22/23 --
# genuinely clean dates in this app's own calendar (no sanctoral entry on
# any of the five, confirmed against data/calendar/sanctoral-calendar.yml).
#
# 12-20 ("O Clavis David") and 12-21 ("O Oriens") could NOT be
# live-verified the same way: every version/track on the live engine
# routes those two dates through St. Thomas's Vigil/feast (Dec 20/21)
# instead of the plain ferial Proper of the Time, even in the plain
# Tridentine 1570 track -- apparently a real modeling choice of that
# engine (a Vigil of an Apostle actually displacing the ferial Magnificat
# antiphon there), not an app-specific quirk. This app's own
# vigil-of-st-thomas.yml already documents a deliberate divergence from
# that engine on this exact date (the Vigil transfers away entirely here,
# per the ordinary privileged-feria/vigil rule, rather than claiming
# Second Vespers the way the live engine's Mass-only-commemoration
# modeling implies) -- so the plain "O Clavis David"/"O Oriens" text
# below, drawn from the standard, universally-fixed wording of these two
# antiphons (unchanged across every edition of the Roman/Monastic
# Breviary), is marked `draft` rather than `verified`: real, correctly
# sourced liturgical text, just not confirmed against this app's own
# primary live reference for lack of a clean query path.
"12-17":
antiphon:
la: "O Sapiéntia, * quæ ex ore Altíssimi prodiísti, attíngens a fine usque ad finem, fórtiter suavitérque dispónens ómnia: veni ad docéndum nos viam prudéntiæ."
en: "O Wisdom, * that comest out of the mouth of the Most High, that reachest from one end to another, and dost mightily and sweetly order all things; come, to teach us the way of prudence!"
"12-18":
antiphon:
la: "O Adonái, * et Dux domus Israël, qui Móysi in igne flammæ rubi apparuísti, et ei in Sina legem dedísti: veni ad rediméndum nos in brácchio exténto."
en: "O Adonai, * and Ruler of the house of Israel, who didst appear unto Moses in the burning bush, and gavest him the law in Sinai; come, to redeem us with an outstretched arm!"
"12-19":
antiphon:
la: "O radix Jesse * qui stas in signum populórum, super quem continébunt reges os suum, quem gentes deprecabúntur: veni ad liberándum nos, jam noli tardáre."
en: "O Root of Jesse, * which standest for an ensign of the people, at whom the kings shall shut their mouths, to whom the Gentiles shall seek; come to deliver us, make no tarrying!"
"12-20":
antiphon:
la: "O clavis David, * et sceptrum domus Israël; qui áperis, et nemo claudit; claudis, et nemo áperit: veni, et educ vinctum de domo cárceris, sedéntem in ténebris, et umbra mortis."
en: "O Key of David, * and Sceptre of the house of Israel, who openest and no man shutteth, who shuttest and no man openeth; come, and bring forth the prisoner from the prison-house, and him that sitteth in darkness and the shadow of death."
status: { la: draft, en: draft }
"12-21":
antiphon:
la: "O Oriens, * splendor lucis ætérnæ, et sol justítiæ: veni, et illúmina sedéntes in ténebris, et umbra mortis."
en: "O Dayspring, * Brightness of light everlasting, and Sun of righteousness; come, and enlighten them that sit in darkness and the shadow of death."
status: { la: draft, en: draft }
"12-22":
antiphon:
la: "O Rex géntium, * et desiderátus eárum, lapísque anguláris, qui facis útraque unum: veni, et salva hóminem, quem de limo formásti."
en: "O King of the Gentiles, * yea, and Desire thereof, O Cornerstone that makest of twain one; come to save man, whom thou hast made of the dust of the earth!"
"12-23":
antiphon:
la: "O Emmánuel, * Rex et légifer noster, exspectátio géntium, et Salvátor eárum: veni ad salvándum nos, Dómine, Deus noster."
en: "O Emmanuel, our King * and our Law-giver, Longing of the Gentiles, yea, and Salvation thereof; come to save us, O Lord our God!"
+36 -3
View File
@@ -9,6 +9,7 @@ import { isInTriduum } from '../calendar/temporal';
import { splitAntiphon, isDoubleOrHigher } from './antiphon'; import { splitAntiphon, isDoubleOrHigher } from './antiphon';
import { getLaudsSaintOverride } from './lauds-psalmody-overrides'; import { getLaudsSaintOverride } from './lauds-psalmody-overrides';
import vespersMagnificatAntiphonsData from '../data/hours/vespers-magnificat-antiphons.yml'; import vespersMagnificatAntiphonsData from '../data/hours/vespers-magnificat-antiphons.yml';
import vespersOAntiphonsData from '../data/hours/vespers-o-antiphons.yml';
type BilingualText = Partial<Record<string, string>>; type BilingualText = Partial<Record<string, string>>;
type TranslationStatus = 'verified' | 'draft' | 'missing'; type TranslationStatus = 'verified' | 'draft' | 'missing';
@@ -21,6 +22,10 @@ interface MagnificatWeekdayDefault {
const vespersMagnificatAntiphons = vespersMagnificatAntiphonsData as Partial< const vespersMagnificatAntiphons = vespersMagnificatAntiphonsData as Partial<
Record<Weekday, MagnificatWeekdayDefault> Record<Weekday, MagnificatWeekdayDefault>
>; >;
// Keyed by "MM-DD" (Dec 17-23) — see the data file's own header for why
// 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>>;
function toResolvedText(proper: ProperText): ResolvedText { function toResolvedText(proper: ProperText): ResolvedText {
return { text: proper.text, status: proper.status, citation: proper.citation }; return { text: proper.text, status: proper.status, citation: proper.citation };
@@ -817,15 +822,43 @@ export function getBenedictusAntiphon(day: LiturgicalDay): ResolvedText {
* order exactly (temporal id, then a saint's own `${propers}-magnificat- * order exactly (temporal id, then a saint's own `${propers}-magnificat-
* antiphon`, then their Common category's `magnificat-antiphon-${common}` * antiphon`, then their Common category's `magnificat-antiphon-${common}`
* — none of that content authored yet, a separate future pass the same * — none of that content authored yet, a separate future pass the same
* shape as the Benedictus one was), but with one real difference: a bare * shape as the Benedictus one was), but with two real differences: a bare
* ferial weekday (Mon-Sat) has actual content to fall back to — * ferial weekday (Mon-Sat) has actual content to fall back to —
* data/hours/vespers-magnificat-antiphons.yml's classic "quote the * data/hours/vespers-magnificat-antiphons.yml's classic "quote the
* Magnificat's own text in sequence" set — which Benedictus has no * Magnificat's own text in sequence" set — which Benedictus has no
* equivalent of. `withPaschaltideAlleluia` applied to that fallback too, * equivalent of; and the O Antiphons below, checked before either the
* by analogy with the Common-category treatment above; not independently * temporal or sanctoral branch even runs.
*
* `withPaschaltideAlleluia` applied to the weekday fallback too, by
* analogy with the Common-category treatment above; not independently
* confirmed for this specific case. * confirmed for this specific case.
*
* The O Antiphons (Dec 17-23) are checked first, unconditionally —
* ahead of *both* the per-week `${winner.id}-magnificat-antiphon` file
* and a real sanctoral winner's own antiphon, deliberately: this is the
* app's own design choice (2026-08-30 user instruction), not a
* reconstruction of any single historical rite's rubric — the reference
* engine itself doesn't do this uniformly (its own Dec 20/21 live query
* shows St. Thomas's own antiphon instead, see vespers-o-antiphons.yml's
* header), but this app deliberately gives the O Antiphon absolute
* priority for this one specific text slot on these seven dates,
* regardless of who otherwise wins the day. Every other part of the
* office (chapter/responsory/hymn/versicle/collect, Lauds' own Benedictus
* antiphon) is untouched by this — a saint who wins Dec 17-23 (St.
* Thomas, Dec 21) still gets everything else that comes with winning,
* just not this one slot. This also means a saint's own real Magnificat
* antiphon, once authored, is permanently unreachable on one of these
* seven dates under *this* calendar's own construction — deliberately
* accepted, since a saint's real antiphon is still worth having on file
* for a differently-constructed calendar (e.g. a future user-editable
* one) even where the shipped calendar's own precedence never surfaces
* it here.
*/ */
export function getMagnificatAntiphon(day: LiturgicalDay): ResolvedText { export function getMagnificatAntiphon(day: LiturgicalDay): ResolvedText {
const oAntiphon = vespersOAntiphons[day.date.slice(5)];
if (oAntiphon) {
return oAntiphon.status ? { text: oAntiphon.antiphon, status: oAntiphon.status } : verifiedText(oAntiphon.antiphon);
}
const winner = resolveOfficeWinner(day); const winner = resolveOfficeWinner(day);
if (winner.kind === 'temporal') { if (winner.kind === 'temporal') {
const named = toResolvedText(getTemporalProper(`${winner.id}-magnificat-antiphon`)); const named = toResolvedText(getTemporalProper(`${winner.id}-magnificat-antiphon`));
+40
View File
@@ -3,6 +3,7 @@ import { resolveDay } from '../../src/calendar';
import { import {
getDayCollect, getDayCollect,
getBenedictusAntiphon, getBenedictusAntiphon,
getMagnificatAntiphon,
resolveMinorHourAntiphon, resolveMinorHourAntiphon,
resolveMinorHourChapter, resolveMinorHourChapter,
splitNamedAntiphon, splitNamedAntiphon,
@@ -305,3 +306,42 @@ describe('Paschaltide alleluia suffix on Common-category antiphons (2026-08)', (
expect(benedictus.text.la?.match(/allelúja/g)?.length).toBe(2); expect(benedictus.text.la?.match(/allelúja/g)?.length).toBe(2);
}); });
}); });
describe('getMagnificatAntiphon — classic weekday-default fallback', () => {
it('falls back to the classic weekday-quote antiphon for a plain temporal id with no proper Magnificat antiphon of its own', () => {
// Synthetic day, not a real resolveOrdo/resolveDay date: after the
// full temporal-cycle Magnificat-antiphon sweep (2026-08-30, see
// TODO.md), every real temporal id this app's calendar can actually
// land Vespers on has its own proper antiphon authored (or is covered
// date-by-date by the O Antiphons, Dec 17-23 -- see
// vespers-o-antiphons.yml), so there's no live date left to exercise
// this fallback tier through the real pipeline any more -- see
// tests/hours/vespers.test.ts's own note where this test used to
// live. Tests the mechanism directly instead, the same way this
// file's other synthetic-day cases do.
const day: LiturgicalDay = {
date: '2026-01-01',
weekday: 'tuesday',
season: 'trinitytide',
temporalCategory: 'ordinary-feria',
winner: { kind: 'temporal', id: 'not-a-real-temporal-id' },
commemorations: [],
};
const magnificat = getMagnificatAntiphon(day);
expect(magnificat.text.la).toContain('Exsúltet');
expect(magnificat.status.la).toBe('verified');
});
it('has no weekday default for Sunday (its own antiphon varies by Proper of the Time)', () => {
const day: LiturgicalDay = {
date: '2026-01-04',
weekday: 'sunday',
season: 'trinitytide',
temporalCategory: 'ordinary-sunday',
winner: { kind: 'temporal', id: 'not-a-real-temporal-id' },
commemorations: [],
};
const magnificat = getMagnificatAntiphon(day);
expect(magnificat.status.la).toBe('missing');
});
});
+49 -14
View File
@@ -248,20 +248,18 @@ describe('resolveOrdo("vespers", ...)', () => {
expect(versicle).toBeDefined(); expect(versicle).toBeDefined();
}); });
it('falls back to the classic weekday-default Magnificat antiphon on a bare ferial', () => { // The classic weekday-default Magnificat antiphon (getMagnificatAntiphon's
// 2033-09-06 is a Tuesday, plain ordinary trinitytide, no saint, no // last fallback tier, vespers-magnificat-antiphons.yml) now has no live,
// active octave (same date already used by calendar/vespers.test.ts's // reachable date left in this app's own calendar to exercise it through
// "does not anticipate between two plain ferias" -- see that test's // resolveOrdo: every temporal id has its own proper antiphon authored
// own comment for why this moved off 2026-06-16). // (the 2026-08-30 full sweep) except advent-4 (now covered date-by-date
const ordo = resolveOrdo('vespers', '2033-09-06'); // by the O Antiphons for its entire practical range, see the "O
const magnificatIndex = ordo.parts.findIndex((p) => p.kind === 'canticle' && p.canticleId === 'magnificat'); // Antiphons" describe block below) and marian-saturday (whose own
expect(magnificatIndex).toBeGreaterThanOrEqual(0); // Vespers is itself unreachable -- Sunday always claims First Vespers
const magnificat = ordo.parts[magnificatIndex]; // over the preceding Saturday evening regardless of Sunday's own rank,
expect(magnificat?.kind === 'canticle' ? magnificat.text.text.la : undefined).toContain('Magníficat ✠ ánima mea'); // see calendar/vespers.ts's hasFirstVespers). See
// The closing full-antiphon repeat is the very next part. // tests/hours/resolve-common.test.ts's own synthetic-day unit test for
const closingAntiphon = ordo.parts[magnificatIndex + 1]; // this fallback tier instead.
expect(closingAntiphon?.kind === 'antiphon' ? closingAntiphon.text.text.la : undefined).toContain('Exsúltet');
});
it('folds the cross-day Vespers commemoration into day-collects (St. James keeps Vespers, St. Anne commemorated)', () => { it('folds the cross-day Vespers commemoration into day-collects (St. James keeps Vespers, St. Anne commemorated)', () => {
// St. Christopher (same-day collision, commemorated under James // St. Christopher (same-day collision, commemorated under James
@@ -664,6 +662,43 @@ describe('resolveOrdo("vespers", ...)', () => {
}); });
}); });
describe('resolveOrdo("vespers", ...) O Antiphons (Dec 17-23)', () => {
it('uses the date-keyed O Antiphon instead of the enclosing Advent week\'s own Magnificat antiphon', () => {
// Regression test for the 2026-08-30 Magnificat-antiphon gap: 2026-12-19
// (a Saturday, genuinely clean -- no saint on this date, see
// data/calendar/sanctoral-calendar.yml) falls within whichever Advent
// week governs that year (advent-3 or advent-4), but the O Antiphon
// "O radix Jesse" must win regardless of which week id that is --
// see vespers-o-antiphons.yml's own header for why these seven can't
// live in a per-week temporal-proper file.
const ordo = resolveOrdo('vespers', '2026-12-19');
const magnificat = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId === 'magnificat');
expect(magnificat?.kind === 'canticle' ? magnificat.antiphon?.text.la : undefined).toContain('radix Jesse');
const magnificatIndex = ordo.parts.indexOf(magnificat!);
const closingAntiphon = ordo.parts[magnificatIndex + 1];
expect(closingAntiphon?.kind === 'antiphon' ? closingAntiphon.text.text.en : undefined).toContain('Root of Jesse');
});
it("gives the O Antiphon absolute priority for the Magnificat slot even over a real sanctoral winner (St. Thomas, Dec 21, Duplex II. classis), while leaving the rest of his own office untouched", () => {
// Direct user instruction (2026-08-30): the O Antiphon always wins
// the Magnificat slot on Dec 17-23, full stop, regardless of who
// otherwise wins the day -- St. Thomas still wins everything else
// about Dec 21 (his own chapter/responsory/hymn/versicle, confirmed
// below), just not this one text. See vespers-o-antiphons.yml's own
// header for the reasoning (a deliberate app-level design choice, not
// a reconstruction of the reference engine's own Dec 21 behavior).
const ordo = resolveOrdo('vespers', '2026-12-21');
const magnificat = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId === 'magnificat');
expect(magnificat?.kind === 'canticle' ? magnificat.antiphon?.text.la : undefined).toContain('Oriens');
expect(magnificat?.kind === 'canticle' ? magnificat.antiphon?.text.en : undefined).toContain('Dayspring');
// The rest of his own office is untouched by this.
const chapter = ordo.parts.find((p) => p.kind === 'chapter');
expect(chapter?.kind === 'chapter' ? chapter.text.status.la : undefined).not.toBe('missing');
const hymn = ordo.parts.find((p) => p.kind === 'hymn');
expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toBeTruthy();
});
});
describe('resolveOrdo("vespers", ...) responsory Gloria omission', () => { describe('resolveOrdo("vespers", ...) responsory Gloria omission', () => {
// Same shared mechanism/rule as Lauds' — see tests/hours/lauds.test.ts's // Same shared mechanism/rule as Lauds' — see tests/hours/lauds.test.ts's
// and tests/hours/compline.test.ts's own describe blocks. // and tests/hours/compline.test.ts's own describe blocks.