Stop gating Lauds/Vespers proper psalmody antiphons behind rank
getPsalmodyOverrideFor bundled "which psalm numbers are said" and "which antiphons are used" into one lookup, both gated behind the same duplex-majus+ rank threshold. A saint's own authored proper antiphon text should never be gated behind rank — only the weaker, generic Common-category substitute has any business being rank-gated. Splits the lookup into two independent tiers: the feast's own proper (any rank, via the new getPsalmodyProperOverrideId) tried first, then the existing duplex-majus+ Common-category fallback — mirroring the split getMinorHourOverrideId already made for the chapter/responsory/hymn/ versicle bundle. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VFGHb4XMe6Wya4pGpPhEEi
This commit is contained in:
+32
-15
@@ -4,7 +4,7 @@ import { resolveDay } from '../calendar';
|
|||||||
import { getDayLabel } from '../calendar/day-label';
|
import { getDayLabel } from '../calendar/day-label';
|
||||||
import { getPsalmVerses } from '../psalter';
|
import { getPsalmVerses } from '../psalter';
|
||||||
import { getCanticle } from './lauds-canticles';
|
import { getCanticle } from './lauds-canticles';
|
||||||
import { getLaudsPsalmodyOverride, type LaudsPsalmodyOverride } from './lauds-psalmody-overrides';
|
import { getLaudsSaintOverride, getLaudsCommonOverride, type LaudsPsalmodyOverride } from './lauds-psalmody-overrides';
|
||||||
import { getOpeningVersicleId } from './opening-versicle';
|
import { getOpeningVersicleId } from './opening-versicle';
|
||||||
import { getMarianAntiphonId, getMarianAntiphonLabel } from './marian-antiphon';
|
import { getMarianAntiphonId, getMarianAntiphonLabel } from './marian-antiphon';
|
||||||
import { applyFlexaMark } from './antiphon';
|
import { applyFlexaMark } from './antiphon';
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
seasonalOfficeSuffix,
|
seasonalOfficeSuffix,
|
||||||
isFerialOrVigil,
|
isFerialOrVigil,
|
||||||
getOfficeOverrideId,
|
getOfficeOverrideId,
|
||||||
|
getPsalmodyProperOverrideId,
|
||||||
resolveResponsory,
|
resolveResponsory,
|
||||||
resolveOfficeBundle,
|
resolveOfficeBundle,
|
||||||
resolveSuffrages,
|
resolveSuffrages,
|
||||||
@@ -49,24 +50,40 @@ interface PsalmodyBlock {
|
|||||||
const laudsAntiphons = laudsAntiphonsData as Record<Weekday, PsalmodyBlock>;
|
const laudsAntiphons = laudsAntiphonsData as Record<Weekday, PsalmodyBlock>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A duplex-majus+ saint's own proper psalmody (per-feast, not per-Common —
|
* Two independent tiers, tried in order:
|
||||||
* explicit choice), or one of the named temporal feasts eligible via
|
|
||||||
* resolve-common.ts's getOfficeOverrideId. Falls back to `undefined` — the
|
|
||||||
* plain weekday default — for an eligible feast with no override authored
|
|
||||||
* yet, same honest incremental-content convention as everywhere else in
|
|
||||||
* this codebase; it's not gated behind whether content exists, just
|
|
||||||
* behind whether it's *eligible* to override at all.
|
|
||||||
*
|
*
|
||||||
* getOfficeOverrideId itself checks resolveOfficeWinner(day), not the raw
|
* 1. **Proper** — this feast's own authored antiphons (per-feast, not
|
||||||
* `day.winner` — on an octave day this is the octave's own feast (e.g. St.
|
* per-Common — explicit choice), eligible at *any rank* via
|
||||||
* Lawrence, days 2-8 of his own octave), which is exactly what should
|
* resolve-common.ts's getPsalmodyProperOverrideId. A saint's own real
|
||||||
* supply the psalmody override there too, live-verified alongside the
|
* proper text is never gated behind rank — see that function's own
|
||||||
* collect/Benedictus-antiphon fix (see resolve-common.ts's
|
* doc comment for why this used to be wrongly bundled with tier 2's
|
||||||
|
* gate (fixed 2026-08-29).
|
||||||
|
* 2. **Common category** — a generic-by-category substitute, still
|
||||||
|
* gated at duplex-majus+ via getOfficeOverrideId (a real, distinct,
|
||||||
|
* weaker tier: same conventional psalm numbers, not this feast's own
|
||||||
|
* text).
|
||||||
|
*
|
||||||
|
* Falls back to `undefined` — the plain weekday default — for an eligible
|
||||||
|
* feast with no override authored yet in either tier, same honest
|
||||||
|
* incremental-content convention as everywhere else in this codebase;
|
||||||
|
* it's not gated behind whether content exists, just behind whether it's
|
||||||
|
* *eligible* to override at all.
|
||||||
|
*
|
||||||
|
* Both id-eligibility functions check resolveOfficeWinner(day), not the
|
||||||
|
* raw `day.winner` — on an octave day this is the octave's own feast
|
||||||
|
* (e.g. St. Lawrence, days 2-8 of his own octave), which is exactly what
|
||||||
|
* should supply the psalmody override there too, live-verified alongside
|
||||||
|
* the collect/Benedictus-antiphon fix (see resolve-common.ts's
|
||||||
* resolveOfficeWinner doc comment).
|
* resolveOfficeWinner doc comment).
|
||||||
*/
|
*/
|
||||||
function getPsalmodyOverrideFor(day: LiturgicalDay): LaudsPsalmodyOverride | undefined {
|
function getPsalmodyOverrideFor(day: LiturgicalDay): LaudsPsalmodyOverride | undefined {
|
||||||
const id = getOfficeOverrideId(day);
|
const properId = getPsalmodyProperOverrideId(day);
|
||||||
return id ? getLaudsPsalmodyOverride(id) : undefined;
|
const proper = properId ? getLaudsSaintOverride(properId) : undefined;
|
||||||
|
if (proper) {
|
||||||
|
return proper;
|
||||||
|
}
|
||||||
|
const categoryId = getOfficeOverrideId(day);
|
||||||
|
return categoryId ? getLaudsCommonOverride(categoryId) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mon-Fri share one capitulum verbatim (confirmed against the live engine
|
// Mon-Fri share one capitulum verbatim (confirmed against the live engine
|
||||||
|
|||||||
@@ -180,6 +180,34 @@ export function getOfficeOverrideId(day: LiturgicalDay): string | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id to look up for today's winner's own *proper* Lauds/Vespers
|
||||||
|
* psalmody antiphons (`hours/lauds.ts`'s and `hours/vespers.ts`'s own
|
||||||
|
* `getPsalmodyOverrideFor`) — eligible at **any rank**, unlike
|
||||||
|
* `getOfficeOverrideId` above. A saint's own authored proper antiphon
|
||||||
|
* text isn't something rank should ever gate — rank only has standing to
|
||||||
|
* gate the weaker, generic Common-category substitute (still looked up
|
||||||
|
* via `getOfficeOverrideId`'s duplex-majus+ threshold, a real distinct
|
||||||
|
* tier: same conventional psalm numbers, a generic-by-category antiphon,
|
||||||
|
* not this feast's own text). Mirrors `getMinorHourOverrideId`'s identical
|
||||||
|
* "any sanctoral winner is eligible, it's the caller that decides whether
|
||||||
|
* anything's actually authored for the id" reasoning — the psalmody
|
||||||
|
* override table previously reused `getOfficeOverrideId`'s single
|
||||||
|
* duplex-majus+ gate for both its proper and Common tiers at once, which
|
||||||
|
* silently discarded a proper antiphon whenever it happened to be
|
||||||
|
* authored for a sub-duplex-majus feast (fixed 2026-08-29).
|
||||||
|
*/
|
||||||
|
export function getPsalmodyProperOverrideId(day: LiturgicalDay): string | undefined {
|
||||||
|
const winner = resolveOfficeWinner(day);
|
||||||
|
if (winner.kind === 'temporal' && ALWAYS_OVERRIDE_TEMPORAL_IDS.has(winner.id)) {
|
||||||
|
return winner.id;
|
||||||
|
}
|
||||||
|
if (winner.kind === 'sanctoral') {
|
||||||
|
return winner.id;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The id to look up for today's office winner's (resolveOfficeWinner)
|
* The id to look up for today's office winner's (resolveOfficeWinner)
|
||||||
* minor-hour content (antiphon + chapter) — `${hourId}-antiphon-${id}`/
|
* minor-hour content (antiphon + chapter) — `${hourId}-antiphon-${id}`/
|
||||||
|
|||||||
+26
-14
@@ -5,7 +5,7 @@ import { weekdayOf } from '../calendar/weekday';
|
|||||||
import { getDayLabel } from '../calendar/day-label';
|
import { getDayLabel } from '../calendar/day-label';
|
||||||
import { getPsalmVerses } from '../psalter';
|
import { getPsalmVerses } from '../psalter';
|
||||||
import { getOpeningVersicleId } from './opening-versicle';
|
import { getOpeningVersicleId } from './opening-versicle';
|
||||||
import { getVespersPsalmodyOverride } from './vespers-psalmody-overrides';
|
import { getVespersSaintOverride, getVespersCommonOverride } from './vespers-psalmody-overrides';
|
||||||
import { applyFlexaMark } from './antiphon';
|
import { applyFlexaMark } from './antiphon';
|
||||||
import {
|
import {
|
||||||
resolveCommon,
|
resolveCommon,
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
seasonalOfficeSuffix,
|
seasonalOfficeSuffix,
|
||||||
isFerialOrVigil,
|
isFerialOrVigil,
|
||||||
getOfficeOverrideId,
|
getOfficeOverrideId,
|
||||||
|
getPsalmodyProperOverrideId,
|
||||||
resolveResponsory,
|
resolveResponsory,
|
||||||
resolveOfficeBundle,
|
resolveOfficeBundle,
|
||||||
resolveSuffrages,
|
resolveSuffrages,
|
||||||
@@ -70,20 +71,31 @@ function psalmParts(group: VespersGroup, day: LiturgicalDay): ResolvedPart[] {
|
|||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A duplex-majus+ saint's own proper Vespers psalmody (none authored
|
/** Two independent tiers, tried in order — same split as hours/lauds.ts's
|
||||||
* yet), or their Common's (live-verified 2026-08-28, see
|
* own getPsalmodyOverrideFor, see its doc comment for the full rationale:
|
||||||
* hours/vespers-psalmody-overrides.ts's own doc comment for why this
|
*
|
||||||
* mechanism is genuinely per-Common — unlike Lauds, the psalm *numbers*
|
* 1. **Proper** — this feast's own authored psalmody, eligible at *any
|
||||||
* themselves differ by Common here, not just the antiphons). Eligibility
|
* rank* via resolve-common.ts's getPsalmodyProperOverrideId (a saint's
|
||||||
* follows the evening's governing `day` (same getOfficeOverrideId test
|
* own real proper text is never gated behind rank).
|
||||||
* as the chapter/responsory/hymn/versicle bundle below) — a First
|
* 2. **Common category** — per hours/vespers-psalmody-overrides.ts's own
|
||||||
* Vespers of a majus+ feast should get that feast's own psalms, same as
|
* doc comment, genuinely per-Common here (the psalm *numbers*
|
||||||
* it gets that feast's own office. Falls back to `undefined` for an
|
* themselves differ by Common, not just the antiphons, unlike Lauds)
|
||||||
* eligible feast with no override authored yet, same honest incremental-
|
* — still gated at duplex-majus+ via getOfficeOverrideId.
|
||||||
* content convention as everywhere else. */
|
*
|
||||||
|
* Eligibility follows the evening's governing `day` (same
|
||||||
|
* resolveOfficeWinner-based test as the chapter/responsory/hymn/versicle
|
||||||
|
* bundle below) — a First Vespers of a feast should get that feast's own
|
||||||
|
* psalms, same as it gets that feast's own office. Falls back to
|
||||||
|
* `undefined` for an eligible feast with no override authored in either
|
||||||
|
* tier, same honest incremental-content convention as everywhere else. */
|
||||||
function getPsalmodyOverrideFor(day: LiturgicalDay): VespersDay | undefined {
|
function getPsalmodyOverrideFor(day: LiturgicalDay): VespersDay | undefined {
|
||||||
const id = getOfficeOverrideId(day);
|
const properId = getPsalmodyProperOverrideId(day);
|
||||||
return id ? getVespersPsalmodyOverride(id) : undefined;
|
const proper = properId ? getVespersSaintOverride(properId) : undefined;
|
||||||
|
if (proper) {
|
||||||
|
return proper;
|
||||||
|
}
|
||||||
|
const categoryId = getOfficeOverrideId(day);
|
||||||
|
return categoryId ? getVespersCommonOverride(categoryId) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The 4 weekday-variable psalm groups — no fixed leading/trailing psalm
|
/** The 4 weekday-variable psalm groups — no fixed leading/trailing psalm
|
||||||
|
|||||||
Reference in New Issue
Block a user