Add the O Antiphons (Dec 17-23) as an unconditional Vespers Magnificat override
Deploy / deploy (push) Successful in 1m27s
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:
@@ -3,6 +3,7 @@ import { resolveDay } from '../../src/calendar';
|
||||
import {
|
||||
getDayCollect,
|
||||
getBenedictusAntiphon,
|
||||
getMagnificatAntiphon,
|
||||
resolveMinorHourAntiphon,
|
||||
resolveMinorHourChapter,
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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
@@ -248,20 +248,18 @@ describe('resolveOrdo("vespers", ...)', () => {
|
||||
expect(versicle).toBeDefined();
|
||||
});
|
||||
|
||||
it('falls back to the classic weekday-default Magnificat antiphon on a bare ferial', () => {
|
||||
// 2033-09-06 is a Tuesday, plain ordinary trinitytide, no saint, no
|
||||
// active octave (same date already used by calendar/vespers.test.ts's
|
||||
// "does not anticipate between two plain ferias" -- see that test's
|
||||
// own comment for why this moved off 2026-06-16).
|
||||
const ordo = resolveOrdo('vespers', '2033-09-06');
|
||||
const magnificatIndex = ordo.parts.findIndex((p) => p.kind === 'canticle' && p.canticleId === 'magnificat');
|
||||
expect(magnificatIndex).toBeGreaterThanOrEqual(0);
|
||||
const magnificat = ordo.parts[magnificatIndex];
|
||||
expect(magnificat?.kind === 'canticle' ? magnificat.text.text.la : undefined).toContain('Magníficat ✠ ánima mea');
|
||||
// The closing full-antiphon repeat is the very next part.
|
||||
const closingAntiphon = ordo.parts[magnificatIndex + 1];
|
||||
expect(closingAntiphon?.kind === 'antiphon' ? closingAntiphon.text.text.la : undefined).toContain('Exsúltet');
|
||||
});
|
||||
// The classic weekday-default Magnificat antiphon (getMagnificatAntiphon's
|
||||
// last fallback tier, vespers-magnificat-antiphons.yml) now has no live,
|
||||
// reachable date left in this app's own calendar to exercise it through
|
||||
// resolveOrdo: every temporal id has its own proper antiphon authored
|
||||
// (the 2026-08-30 full sweep) except advent-4 (now covered date-by-date
|
||||
// by the O Antiphons for its entire practical range, see the "O
|
||||
// Antiphons" describe block below) and marian-saturday (whose own
|
||||
// Vespers is itself unreachable -- Sunday always claims First Vespers
|
||||
// over the preceding Saturday evening regardless of Sunday's own rank,
|
||||
// see calendar/vespers.ts's hasFirstVespers). See
|
||||
// tests/hours/resolve-common.test.ts's own synthetic-day unit test for
|
||||
// this fallback tier instead.
|
||||
|
||||
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
|
||||
@@ -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', () => {
|
||||
// Same shared mechanism/rule as Lauds' — see tests/hours/lauds.test.ts's
|
||||
// and tests/hours/compline.test.ts's own describe blocks.
|
||||
|
||||
Reference in New Issue
Block a user