Render octave commemorations in getDayCollects on stacking days

getDayCollects previously dropped kind: 'octave' commemorations
entirely, so on days where the temporal office keeps its own standing
(the Christmas Octave's own Nativity/Stephen/John/Holy-Innocents stack,
e.g. Dec 30) those octaves contributed nothing but a name in the day
label. Add an octave branch, gated on temporalCategory !== 'ordinary-
feria' -- the same condition resolveOfficeWinner uses -- so an ordinary
octave day's already-substituted primary content never gets a redundant
repeat.
This commit is contained in:
2026-08-17 21:32:05 -04:00
parent f60a142bb4
commit 6f1e152d7f
2 changed files with 62 additions and 4 deletions
+34 -4
View File
@@ -462,6 +462,22 @@ function sanctoralCommemorationPart(commemoration: Extract<Commemoration, { kind
return { kind: 'preces', text: { text: {}, status: { la: 'missing', en: 'missing' } }, label };
}
/** An octave commemoration's own rendering — the fuller "Commemoratio
* Octavæ ..." Ant+V/R+collect bundle (`${id}-octave-commemoration`, e.g.
* christmas-day-octave-commemoration.yml), same shape and fallback
* pattern as sanctoralCommemorationPart. No per-saint `propers`
* indirection needed: an octave commemoration's id already is the base
* feast/saint id (`christmas-day`, `st-stephen-protomartyr`, ...),
* matching the proper file's own id 1:1. */
function octaveCommemorationPart(commemoration: Extract<Commemoration, { kind: 'octave' }>): ResolvedPart {
const label = `Commemoration of the Octave of ${commemoration.name}`;
const combined = resolveCommon(`${commemoration.id}-octave-commemoration`);
if (combined.status.la !== 'missing' || combined.status.en !== 'missing') {
return { kind: 'preces', text: combined, label };
}
return { kind: 'preces', text: { text: {}, status: { la: 'missing', en: 'missing' } }, label };
}
/**
* The day's own collect, plus one more per commemoration (calendar/
* types.ts's LiturgicalDay.commemorations) — Lauds/Vespers say all of
@@ -475,10 +491,22 @@ function sanctoralCommemorationPart(commemoration: Extract<Commemoration, { kind
* Amen." for the single-collect case every other hour uses today, and
* stripping that back out per-collect to chain them properly would need
* text surgery this doesn't attempt — so on a commemorated day, each
* collect here renders as its own complete, separate block instead. An
* octave commemoration never contributes anything of its own here — once
* resolveOfficeWinner is in play, its content is already the primary
* collect above, so a separate entry would just be a redundant repeat.
* collect here renders as its own complete, separate block instead.
*
* An octave commemoration only renders its own block here when
* `day.temporalCategory !== 'ordinary-feria'` — the same gate
* resolveOfficeWinner uses (see its own doc comment). On an *ordinary*
* octave day (e.g. an in-between day of St. Lawrence's own octave),
* resolveOfficeWinner has already substituted that octave's content as
* the primary collect above, so rendering it again here would be a
* redundant repeat (or worse, an honestly-"missing" duplicate for the
* many octaves without their own `-octave-commemoration.yml` authored).
* On a day the temporal identity itself keeps real standing (the
* Christmas Octave's own stacking days, e.g. Dec 26-31, where Christmas
* + Stephen + John + Holy Innocents are each merely commemorated
* alongside the day's own temporal office), resolveOfficeWinner does
* *not* touch the primary collect at all, so this is the only place
* these four octaves' real "Commemoratio Octavæ ..." content surfaces.
*/
export function getDayCollects(day: LiturgicalDay): ResolvedPart[] {
const parts: ResolvedPart[] = [{ kind: 'prayer', text: getDayCollect(day) }];
@@ -487,6 +515,8 @@ export function getDayCollects(day: LiturgicalDay): ResolvedPart[] {
parts.push({ kind: 'prayer', text: toResolvedText(getTemporalProper(`${commemoration.id}-collect`)) });
} else if (commemoration.kind === 'sanctoral') {
parts.push(sanctoralCommemorationPart(commemoration));
} else if (commemoration.kind === 'octave' && day.temporalCategory !== 'ordinary-feria') {
parts.push(octaveCommemorationPart(commemoration));
}
}
return parts;
+28
View File
@@ -157,6 +157,34 @@ describe('resolveOrdo("vespers", ...)', () => {
expect(anne).toBeDefined();
});
it("renders each of the Christmas Octave's own stacked 'Commemoratio Octavæ' blocks on a day the temporal office itself keeps standing", () => {
// 2033-12-30 -- one day past the '2033-12-29' stacking fixture already
// used in tests/calendar/octaves.test.ts (St. Thomas of Canterbury's
// day, itself outside this app's calendar so the day stays plain
// temporal). All four of Christmas/Stephen/John/Holy Innocents' own
// octaves are active and none of them wins the day outright (the
// temporal day itself, "privileged-feria-minor", keeps the office) --
// see hours/resolve-common.ts's getDayCollects, kind === 'octave'
// branch, and its own doc comment for why this is the one case an
// octave commemoration renders its own Ant+V/R+collect block here.
const ordo = resolveOrdo('vespers', '2033-12-30');
const octaveParts = ordo.parts.filter(
(p): p is Extract<typeof p, { kind: 'preces' }> => p.kind === 'preces' && (p.label?.includes('Octave of') ?? false),
);
expect(octaveParts.map((p) => p.label).sort()).toEqual(
[
'Commemoration of the Octave of Christmas',
'Commemoration of the Octave of The Holy Innocents, Martyrs',
'Commemoration of the Octave of St. John, Apostle and Evangelist',
'Commemoration of the Octave of St. Stephen, Protomartyr',
].sort(),
);
for (const part of octaveParts) {
expect(part.text.status.la).toBe('verified');
expect(part.text.status.en).toBe('verified');
}
});
it("renders Advent's seasonal chapter/hymn/versicle instead of the plain weekday default", () => {
// 2025-12-02 is a Tuesday in Advent (St. Bibiana, Simplex, merely
// commemorated — the day's own winner stays temporal either way).