Widen seasonal hymn-doxology to cover Marian Saturday and the Assumption octave
getHymnDoxologyId now takes the whole LiturgicalDay (needed for a date-window check) and checks a feast override for a temporal winner's id too, not just sanctoral — matching Divinum Officium's own Common-of-the-BVM `Doxology=Nat` rule, which is why Marian Saturdays get the Nativity doxology regardless of season. Also ports the source's Aug 16-22 (Assumption octave) hardcoded Nativity-doxology window, confirmed live not excluded for the Monastic/ Tridentine 1617 track. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# Per-feast override of the hymn's doxology stanza — real Divinum Officium
|
# Per-feast override of the hymn's doxology stanza — real Divinum Officium
|
||||||
# has a `winner{Doxology}` mechanism for this (some feasts specify their own).
|
# has a `winner{Doxology}` mechanism for this (some feasts specify their own),
|
||||||
# Empty until milestone 4's sanctoral propers store exists, same as
|
# plus a `Rule Doxology=` mechanism a whole Common can set (e.g. the Common
|
||||||
# prime-chapter-responsory-by-feast.yml.
|
# of the BVM's own `Doxology=Nat` — Commune/C10-C12Q.txt, live-checked
|
||||||
byFeastId: {}
|
# 2026-09-03), which is why marian-saturday is keyed here by its temporal id
|
||||||
|
# even though it's not a "feast" in the sanctoral-propers-store sense; see
|
||||||
|
# hours/hymn-doxology.ts's getHymnDoxologyId for the kind-agnostic lookup.
|
||||||
|
# Otherwise empty until milestone 4's sanctoral propers store exists, same
|
||||||
|
# as prime-chapter-responsory-by-feast.yml.
|
||||||
|
byFeastId:
|
||||||
|
marian-saturday: hymn-doxology-nat
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
|||||||
return [{ kind: 'hymn', text: resolveCommon('compline-hymn-lenten') }];
|
return [{ kind: 'hymn', text: resolveCommon('compline-hymn-lenten') }];
|
||||||
}
|
}
|
||||||
const body = resolveCommon(part.textRef.id);
|
const body = resolveCommon(part.textRef.id);
|
||||||
const doxology = resolveCommon(
|
const doxology = resolveCommon(getHymnDoxologyId(day, 'compline-hymn-doxology-per-annum'));
|
||||||
getHymnDoxologyId(day.season, day.winner, 'compline-hymn-doxology-per-annum'),
|
|
||||||
);
|
|
||||||
return [{ kind: 'hymn', text: appendDoxology(body, doxology) }];
|
return [{ kind: 'hymn', text: appendDoxology(body, doxology) }];
|
||||||
}
|
}
|
||||||
case 'chapter':
|
case 'chapter':
|
||||||
|
|||||||
@@ -1,21 +1,55 @@
|
|||||||
import type { Season, DayWinner } from '../calendar/types';
|
import type { LiturgicalDay } from '../calendar/types';
|
||||||
import { resolveSeasonalPropersId } from './seasonal-propers';
|
import { resolveOfficeWinner } from './resolve-common';
|
||||||
import bySeasonData from '../data/hours/hymn-doxology-by-season.yml';
|
import bySeasonData from '../data/hours/hymn-doxology-by-season.yml';
|
||||||
import byFeastData from '../data/hours/hymn-doxology-by-feast.yml';
|
import byFeastData from '../data/hours/hymn-doxology-by-feast.yml';
|
||||||
|
|
||||||
const bySeason = bySeasonData as { bySeason: Record<string, string> };
|
const bySeason = bySeasonData as { bySeason: Record<string, string> };
|
||||||
const byFeast = byFeastData as { byFeastId: Record<string, string> };
|
const byFeast = byFeastData as { byFeastId: Record<string, string> };
|
||||||
|
|
||||||
|
const NAT_DOXOLOGY_ID = 'hymn-doxology-nat';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Divinum Officium's own doxology() sub (specials/hymni.pl) hardcodes the
|
||||||
|
* Assumption's octave (Aug 16-22) to the Nativity doxology, regardless of
|
||||||
|
* season or which feast is winning that day — live-checked in the source,
|
||||||
|
* and *not* excluded for the Monastic/Tridentine 1617 track (unlike its
|
||||||
|
* Dec 9-15 companion rule for the Immaculate Conception's run-up, which the
|
||||||
|
* source explicitly excludes 1570/1617/1963/altovadensis from, so that one
|
||||||
|
* is deliberately NOT ported here). Checked directly by date, same pattern
|
||||||
|
* as marian-antiphon.ts's isCandlemasToHolyWednesday — this window doesn't
|
||||||
|
* line up with any `Season` value.
|
||||||
|
*/
|
||||||
|
function isAssumptionOctaveWindow(isoDate: string): boolean {
|
||||||
|
const [, monthStr, dayStr] = isoDate.split('-');
|
||||||
|
const month = Number(monthStr);
|
||||||
|
const day = Number(dayStr);
|
||||||
|
return month === 8 && day > 15 && day < 23;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the common-propers id for a hymn's seasonal final doxology
|
* Resolves the common-propers id for a hymn's seasonal final doxology
|
||||||
* stanza. The seasonal overrides are shared across every hymn (Divinum
|
* stanza. The seasonal overrides are shared across every hymn (Divinum
|
||||||
* Officium's own Doxologies.txt table is hymn-agnostic), but the per-annum
|
* Officium's own Doxologies.txt table is hymn-agnostic), but the per-annum
|
||||||
* default is each hymn's own natural ending, so callers supply it.
|
* default is each hymn's own natural ending, so callers supply it.
|
||||||
|
*
|
||||||
|
* Precedence, matching hymni.pl's doxology() sub: (1) an explicit per-feast
|
||||||
|
* override — real Divinum Officium's `winner{Doxology}`/`Rule Doxology=`
|
||||||
|
* mechanism, e.g. the Common of the BVM's own `Doxology=Nat` rule, which is
|
||||||
|
* why Marian Saturdays (a `kind: 'temporal'` winner, not `'sanctoral'` —
|
||||||
|
* checked regardless of kind, unlike chapter-responsory's/opening-
|
||||||
|
* versicle's own byFeastId tables, since a temporal id and a sanctoral id
|
||||||
|
* never collide) get the Nativity doxology even outside Christmastide; (2)
|
||||||
|
* the Assumption-octave date window above; (3) the season; (4) the hymn's
|
||||||
|
* own per-annum default.
|
||||||
*/
|
*/
|
||||||
export function getHymnDoxologyId(season: Season, winner: DayWinner, perAnnumId: string): string {
|
export function getHymnDoxologyId(day: LiturgicalDay, perAnnumId: string): string {
|
||||||
return resolveSeasonalPropersId(season, winner, {
|
const winner = resolveOfficeWinner(day);
|
||||||
perAnnum: perAnnumId,
|
const feastOverride = winner.kind === 'sanctoral' || winner.kind === 'temporal' ? byFeast.byFeastId[winner.id] : undefined;
|
||||||
bySeason: bySeason.bySeason,
|
if (feastOverride) {
|
||||||
byFeastId: byFeast.byFeastId,
|
return feastOverride;
|
||||||
});
|
}
|
||||||
|
if (isAssumptionOctaveWindow(day.date)) {
|
||||||
|
return NAT_DOXOLOGY_ID;
|
||||||
|
}
|
||||||
|
return bySeason.bySeason[day.season] ?? perAnnumId;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ function resolvePart(part: HourPart, date: string, day: LiturgicalDay): Resolved
|
|||||||
switch (part.kind) {
|
switch (part.kind) {
|
||||||
case 'hymn': {
|
case 'hymn': {
|
||||||
const body = resolveCommon(part.textRef.id);
|
const body = resolveCommon(part.textRef.id);
|
||||||
const doxology = resolveCommon(getHymnDoxologyId(day.season, day.winner, 'hymn-doxology-per-annum'));
|
const doxology = resolveCommon(getHymnDoxologyId(day, 'hymn-doxology-per-annum'));
|
||||||
return [{ kind: 'hymn', text: appendDoxology(body, doxology) }];
|
return [{ kind: 'hymn', text: appendDoxology(body, doxology) }];
|
||||||
}
|
}
|
||||||
case 'chapter':
|
case 'chapter':
|
||||||
|
|||||||
Reference in New Issue
Block a user