Lauds/Vespers chapter-hymn bundle: fall back to Common at any rank
resolveOffice in both hours only substituted a feast's own proper chapter/responsory/hymn/versicle, gated at duplex-majus+ (piggybacking eligibility on Lauds' psalmody-override table). A simplex/semiduplex feast with no proper content of its own dropped straight to the plain ferial default, skipping their Common entirely (caught via St. Louis, Simplex, common-of-a-confessor-not-bishop). Add resolve-common.ts's resolveOfficeBundle: tries the winner's own proper bundle first, then their Common's bundle, eligibility tested rank-agnostically via the existing getMinorHourOverrideId (any sanctoral winner) rather than duplex-majus+ — matching the rule already used for the minor hours. Applied to both Lauds and Vespers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HWN869GrMFHqrer9fdCBbF
This commit is contained in:
+18
-20
@@ -21,6 +21,7 @@ import {
|
|||||||
isFerialOrVigil,
|
isFerialOrVigil,
|
||||||
getOfficeOverrideId,
|
getOfficeOverrideId,
|
||||||
resolveResponsory,
|
resolveResponsory,
|
||||||
|
resolveOfficeBundle,
|
||||||
} from './resolve-common';
|
} from './resolve-common';
|
||||||
import laudsDefinitionData from '../data/hours/lauds.yml';
|
import laudsDefinitionData from '../data/hours/lauds.yml';
|
||||||
import laudsAntiphonsData from '../data/hours/lauds-antiphons.yml';
|
import laudsAntiphonsData from '../data/hours/lauds-antiphons.yml';
|
||||||
@@ -178,30 +179,27 @@ function resolvePsalmody(day: LiturgicalDay): ResolvedPart[] {
|
|||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A duplex-majus+/Marian-Saturday override's own capitulum/responsory/
|
/** Any sanctoral winner's (or named-temporal feast's) own capitulum/
|
||||||
* hymn/versicle (data/propers/common/lauds-{part}-${id}.yml), when
|
* responsory/hymn/versicle (data/propers/common/lauds-{part}-${id}.yml),
|
||||||
* authored — most overrides only bother with the psalmody (see
|
* else their Common's (`${part}-${common}`), when authored — see
|
||||||
* getPsalmodyOverrideFor) and leave this bundle at its plain weekday
|
* resolve-common.ts's resolveOfficeBundle for the eligibility/fallback
|
||||||
* default, which is exactly what falls back here when the id-keyed
|
* rule (deliberately rank-agnostic as of 2026-08-25: a simplex feast's
|
||||||
* capitulum doesn't exist (same dual-missing-status "not authored"
|
* Common chapter/hymn is real content, not gated behind duplex-majus+
|
||||||
* signal used throughout this codebase, not a special case). Falls to a
|
* like the psalmody override above is). Falls to a *seasonal* default
|
||||||
* *seasonal* default next (Advent/Lent/Passiontide/Paschaltide — see
|
* next (Advent/Lent/Passiontide/Paschaltide — see resolve-common.ts's
|
||||||
* resolve-common.ts's seasonalOfficeSuffix), and only then the plain
|
* seasonalOfficeSuffix), and only then the plain weekday default — a
|
||||||
* weekday default — a feast's own override always wins over the season
|
* feast's own override always wins over the season it happens to fall
|
||||||
* it happens to fall in, same as everywhere else in this codebase. */
|
* in, same as everywhere else in this codebase. */
|
||||||
function resolveOffice(day: LiturgicalDay): ResolvedPart[] {
|
function resolveOffice(day: LiturgicalDay): ResolvedPart[] {
|
||||||
const overrideId = getPsalmodyOverrideFor(day)?.id;
|
const bundle = resolveOfficeBundle('lauds', day);
|
||||||
if (overrideId) {
|
if (bundle) {
|
||||||
const chapter = resolveCommon(`lauds-capitulum-${overrideId}`);
|
|
||||||
if (chapter.status.la !== 'missing' || chapter.status.en !== 'missing') {
|
|
||||||
return [
|
return [
|
||||||
{ kind: 'chapter', text: chapter },
|
{ kind: 'chapter', text: bundle.chapter },
|
||||||
{ kind: 'responsory', text: resolveResponsory(resolveCommon(`lauds-responsory-${overrideId}`), day) },
|
{ kind: 'responsory', text: bundle.responsory },
|
||||||
{ kind: 'hymn', text: resolveCommon(`lauds-hymn-${overrideId}`) },
|
{ kind: 'hymn', text: bundle.hymn },
|
||||||
{ kind: 'versicle', text: resolveCommon(`lauds-versicle-${overrideId}`) },
|
{ kind: 'versicle', text: bundle.versicle },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
const seasonSuffix = seasonalOfficeSuffix(day.season);
|
const seasonSuffix = seasonalOfficeSuffix(day.season);
|
||||||
const key = seasonSuffix ?? day.weekday;
|
const key = seasonSuffix ?? day.weekday;
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -401,6 +401,66 @@ export function resolveMinorHourChapter(hourId: string, day: LiturgicalDay, fall
|
|||||||
return resolveCommon(fallbackId);
|
return resolveCommon(fallbackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OfficeBundle {
|
||||||
|
chapter: ResolvedText;
|
||||||
|
responsory: ResolvedText;
|
||||||
|
hymn: ResolvedText;
|
||||||
|
versicle: ResolvedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The chapter/responsory/hymn/versicle bundle following Lauds'/Vespers'
|
||||||
|
* psalmody (hours/lauds.ts's and hours/vespers.ts's own resolveOffice) —
|
||||||
|
* a saint's own proper bundle (`${hourId}-{part}-${id}`) when authored,
|
||||||
|
* else that saint's Common bundle (`${hourId}-{part}-${commonId}`,
|
||||||
|
* SaintRecord's main `common` field, not `minorHoursCommon` — this is the
|
||||||
|
* saint's overall classification, the same one driving their day collect).
|
||||||
|
* Eligibility is deliberately rank-agnostic, `getMinorHourOverrideId`'s
|
||||||
|
* test (any sanctoral winner, or a named ALWAYS_OVERRIDE_TEMPORAL_IDS
|
||||||
|
* feast) rather than resolveOffice's old duplex-majus+-only gate: a
|
||||||
|
* simplex feast's own or Common's chapter/hymn is real, distinct
|
||||||
|
* liturgical content, not something only strong feasts are entitled to —
|
||||||
|
* confirmed 2026-08-25 (St. Louis, King of France, simplex, was silently
|
||||||
|
* falling all the way to the plain ferial default instead of his Common
|
||||||
|
* of a Confessor Not Bishop). Same honest "eligible, not gated on whether
|
||||||
|
* content exists" convention as everywhere else: returns `undefined` when
|
||||||
|
* neither the proper nor the Common has this bundle authored yet, so the
|
||||||
|
* caller falls through to its own seasonal/weekday default.
|
||||||
|
*
|
||||||
|
* `resolveChapterFor` lets Vespers reuse its own chapter lookup (which
|
||||||
|
* itself falls back to the byte-identical Lauds capitulum file when no
|
||||||
|
* `vespers-capitulum-<id>.yml` exists — see vespersCapitulumForOverride)
|
||||||
|
* instead of the plain `${hourId}-capitulum-${id}` default used here.
|
||||||
|
*/
|
||||||
|
export function resolveOfficeBundle(
|
||||||
|
hourId: string,
|
||||||
|
day: LiturgicalDay,
|
||||||
|
resolveChapterFor: (id: string) => ResolvedText = (id) => resolveCommon(`${hourId}-capitulum-${id}`),
|
||||||
|
): OfficeBundle | undefined {
|
||||||
|
const overrideId = getMinorHourOverrideId(day);
|
||||||
|
if (!overrideId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const tryId = (id: string): OfficeBundle | undefined => {
|
||||||
|
const chapter = resolveChapterFor(id);
|
||||||
|
if (chapter.status.la === 'missing' && chapter.status.en === 'missing') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
chapter,
|
||||||
|
responsory: resolveResponsory(resolveCommon(`${hourId}-responsory-${id}`), day),
|
||||||
|
hymn: resolveCommon(`${hourId}-hymn-${id}`),
|
||||||
|
versicle: resolveCommon(`${hourId}-versicle-${id}`),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const proper = tryId(overrideId);
|
||||||
|
if (proper) {
|
||||||
|
return proper;
|
||||||
|
}
|
||||||
|
const commonId = getSaintRecord(overrideId)?.common;
|
||||||
|
return commonId ? tryId(commonId) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/** Fills a Common collect template's literal `{N}` placeholders with a
|
/** Fills a Common collect template's literal `{N}` placeholders with a
|
||||||
* saint's own name(s) per language (SaintRecord.collectName) — the same
|
* saint's own name(s) per language (SaintRecord.collectName) — the same
|
||||||
* substitution the reference source's own Commune files perform
|
* substitution the reference source's own Commune files perform
|
||||||
|
|||||||
+22
-23
@@ -15,8 +15,8 @@ import {
|
|||||||
verifiedText,
|
verifiedText,
|
||||||
seasonalOfficeSuffix,
|
seasonalOfficeSuffix,
|
||||||
isFerialOrVigil,
|
isFerialOrVigil,
|
||||||
getOfficeOverrideId,
|
|
||||||
resolveResponsory,
|
resolveResponsory,
|
||||||
|
resolveOfficeBundle,
|
||||||
} from './resolve-common';
|
} from './resolve-common';
|
||||||
import vespersDefinitionData from '../data/hours/vespers.yml';
|
import vespersDefinitionData from '../data/hours/vespers.yml';
|
||||||
import vespersAntiphonsData from '../data/hours/vespers-antiphons.yml';
|
import vespersAntiphonsData from '../data/hours/vespers-antiphons.yml';
|
||||||
@@ -109,32 +109,31 @@ function vespersCapitulumForOverride(id: string): ResolvedText {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The chapter/responsory/hymn/versicle bundle following the psalmody —
|
/** The chapter/responsory/hymn/versicle bundle following the psalmody —
|
||||||
* see hours/types.ts's 'vespers-office' doc comment. A duplex-majus+
|
* see hours/types.ts's 'vespers-office' doc comment. Any sanctoral
|
||||||
* saint's own proper bundle (chapter via vespersCapitulumForOverride
|
* winner's (or named-temporal feast's) own proper bundle (chapter via
|
||||||
* above, responsory/hymn/versicle via `vespers-{part}-${id}`), or one of
|
* vespersCapitulumForOverride above, responsory/hymn/versicle via
|
||||||
* the named temporal feasts eligible via resolve-common.ts's
|
* `vespers-{part}-${id}`), else their Common's bundle, wins first when
|
||||||
* getOfficeOverrideId, wins first when authored — same eligible-but-not-
|
* authored — resolve-common.ts's resolveOfficeBundle, deliberately
|
||||||
* gated-on-content pattern as Lauds' resolveOffice, just without a
|
* rank-agnostic (2026-08-25: matches Lauds' identical change, ditto for
|
||||||
* psalmody-override table of its own to piggyback eligibility on
|
* the same reason — a simplex feast's Common office is real content),
|
||||||
* (Vespers has no per-feast psalmody override mechanism — see TODO.md).
|
* not gated at duplex-majus+ the way this hour's lack of its own
|
||||||
* Falls to a *seasonal* default next (Advent/Lent/Passiontide/
|
* psalmody-override table might otherwise suggest (Vespers has no
|
||||||
* Paschaltide — see resolve-common.ts's seasonalOfficeSuffix), and only
|
* per-feast psalmody override mechanism — see TODO.md; that's unrelated
|
||||||
* then the plain weekday default — a feast's own override always wins
|
* to this bundle's own eligibility). Falls to a *seasonal* default next
|
||||||
* over the season it happens to fall in, same as everywhere else in this
|
* (Advent/Lent/Passiontide/Paschaltide — see resolve-common.ts's
|
||||||
* codebase. */
|
* seasonalOfficeSuffix), and only then the plain weekday default — a
|
||||||
|
* feast's own override always wins over the season it happens to fall
|
||||||
|
* in, same as everywhere else in this codebase. */
|
||||||
function resolveOffice(day: LiturgicalDay): ResolvedPart[] {
|
function resolveOffice(day: LiturgicalDay): ResolvedPart[] {
|
||||||
const overrideId = getOfficeOverrideId(day);
|
const bundle = resolveOfficeBundle('vespers', day, vespersCapitulumForOverride);
|
||||||
if (overrideId) {
|
if (bundle) {
|
||||||
const chapter = vespersCapitulumForOverride(overrideId);
|
|
||||||
if (chapter.status.la !== 'missing' || chapter.status.en !== 'missing') {
|
|
||||||
return [
|
return [
|
||||||
{ kind: 'chapter', text: chapter },
|
{ kind: 'chapter', text: bundle.chapter },
|
||||||
{ kind: 'responsory', text: resolveResponsory(resolveCommon(`vespers-responsory-${overrideId}`), day) },
|
{ kind: 'responsory', text: bundle.responsory },
|
||||||
{ kind: 'hymn', text: resolveCommon(`vespers-hymn-${overrideId}`) },
|
{ kind: 'hymn', text: bundle.hymn },
|
||||||
{ kind: 'versicle', text: resolveCommon(`vespers-versicle-${overrideId}`) },
|
{ kind: 'versicle', text: bundle.versicle },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
const seasonSuffix = seasonalOfficeSuffix(day.season);
|
const seasonSuffix = seasonalOfficeSuffix(day.season);
|
||||||
const key = seasonSuffix ?? day.weekday;
|
const key = seasonSuffix ?? day.weekday;
|
||||||
return [
|
return [
|
||||||
|
|||||||
Reference in New Issue
Block a user