Let a temporal winner reach Matins' 3-nocturn proper psalmody
hours/matins.ts's threeNocturns gate and Sunday-branch dispatch only ever fired for a literal Sunday or a sanctoral winner — a temporal winner (e.g. Easter Monday/Tuesday, sharing Easter Sunday's own winner id) had no path into 3-nocturn proper content at all, even with a matching named override authored for it. Generalizes hours/matins-sunday-named-nocturn-overrides.yml's lookup so a temporal winner with an authored entry is eligible too, reusing the same sundayPsalmNocturn/sundayCanticleNocturn render functions the Sunday and saint-proper branches already share. A new optional weekdays field lets one entry restrict itself to specific days of a temporalId that spans more calendar days than actually share its content (needed by the Easter content this unblocks, added next). Built as a general-purpose feature, not scoped to Easter specifically — any temporal feast that wants its own proper psalms/lessons can now reach this path by adding a named-override entry. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X26BkFT3ARbtUGXuBmzUmD
This commit is contained in:
+36
-13
@@ -170,11 +170,23 @@ const sundayPaschaltideOverrides = matinsSundayPaschaltideOverridesData as unkno
|
|||||||
|
|
||||||
// A specific privileged Sunday whose own proper is wholesale different
|
// A specific privileged Sunday whose own proper is wholesale different
|
||||||
// from the fixed 12-psalm/3-canticle scheme, keyed by `temporalId` — see
|
// from the fixed 12-psalm/3-canticle scheme, keyed by `temporalId` — see
|
||||||
// matins-sunday-named-nocturn-overrides.yml's own header (currently just
|
// matins-sunday-named-nocturn-overrides.yml's own header (currently
|
||||||
// `sunday-after-ascension`). Checked ahead of the season-wide overrides
|
// `sunday-after-ascension` and `easter-sunday`). Checked ahead of the
|
||||||
// above, though in practice they never collide (no season-wide override
|
// season-wide overrides above, though in practice they never collide (no
|
||||||
// exists for `ascensiontide`).
|
// season-wide override exists for `ascensiontide`).
|
||||||
const sundayNamedOverrides = matinsSundayNamedOverridesData as unknown as Record<string, Partial<MatinsSundayNocturnOverrides>>;
|
//
|
||||||
|
// `weekdays` (2026-09-02): optional, for a `temporalId` that spans more
|
||||||
|
// calendar days than actually share this content — e.g. `easter-sunday`
|
||||||
|
// is the winner id for the *whole* Easter Octave (all 6 weekdays), but
|
||||||
|
// only Sunday/Monday/Tuesday of it are live-verified to share this exact
|
||||||
|
// proper; Wednesday-Saturday have their own different (not yet authored)
|
||||||
|
// antiphons. Omitted entirely (as `sunday-after-ascension` does) means
|
||||||
|
// "every day carrying this temporalId" — the common case for a temporal
|
||||||
|
// override whose id already maps to exactly one calendar day.
|
||||||
|
interface MatinsSundayNamedOverride extends Partial<MatinsSundayNocturnOverrides> {
|
||||||
|
weekdays?: Weekday[];
|
||||||
|
}
|
||||||
|
const sundayNamedOverrides = matinsSundayNamedOverridesData as unknown as Record<string, MatinsSundayNamedOverride>;
|
||||||
|
|
||||||
/** Sunday Nocturn I/II/III content for a given day — a per-temporalId
|
/** Sunday Nocturn I/II/III content for a given day — a per-temporalId
|
||||||
* named override (a specific privileged Sunday's own wholesale-different
|
* named override (a specific privileged Sunday's own wholesale-different
|
||||||
@@ -734,17 +746,28 @@ export function resolveOrdo(date: string): ResolvedOrdo {
|
|||||||
const day = resolveDay(date);
|
const day = resolveDay(date);
|
||||||
const winner = resolveOfficeWinner(day);
|
const winner = resolveOfficeWinner(day);
|
||||||
const temporalId = resolveTemporalId(date);
|
const temporalId = resolveTemporalId(date);
|
||||||
// Every Sunday, unconditionally, or a Semiduplex+ sanctoral winner — the
|
// Every Sunday, unconditionally, a Semiduplex+ sanctoral winner — the
|
||||||
// user's own chosen threshold (2026-08, lowered from Duplex to
|
// user's own chosen threshold (2026-08, lowered from Duplex to
|
||||||
// Semiduplex 2026-09-02), not gated on whether any content is actually
|
// Semiduplex 2026-09-02), not gated on whether any content is actually
|
||||||
// authored yet, same "eligible, not content-gated" convention every
|
// authored yet, same "eligible, not content-gated" convention every
|
||||||
// other per-feast override in this app already uses (see
|
// other per-feast override in this app already uses (see
|
||||||
// hours/resolve-common.ts's getOfficeOverrideId). Deliberately a
|
// hours/resolve-common.ts's getOfficeOverrideId) — or a temporal winner
|
||||||
// separate check from `isDoubleOrHigher` (antiphon.ts) — that function
|
// with its own named override (2026-09-02, e.g. Easter Monday/Tuesday
|
||||||
// drives the unrelated antiphon-doubling rule (full vs. incipit antiphon
|
// reusing Easter Sunday's own entry; see `hasTemporalNamedOverride`
|
||||||
// before the psalms) and stays floored at Duplex; the two thresholds
|
// below). Unlike the sanctoral case, a plain temporal id is NOT
|
||||||
// only coincided before this change by accident, not by shared meaning.
|
// eligible just by existing — only one with an authored entry in
|
||||||
const threeNocturns = day.weekday === 'sunday' || (winner.kind === 'sanctoral' && isAtLeast(winner.rank, 'semiduplex'));
|
// `sundayNamedOverrides` is, since ordinary temporal ferias/Sundays
|
||||||
|
// already have their own correct path and shouldn't suddenly gain 3
|
||||||
|
// nocturns. Deliberately a separate check from `isDoubleOrHigher`
|
||||||
|
// (antiphon.ts) — that function drives the unrelated antiphon-doubling
|
||||||
|
// rule (full vs. incipit antiphon before the psalms) and stays floored
|
||||||
|
// at Duplex; the two thresholds only coincided before this change by
|
||||||
|
// accident, not by shared meaning.
|
||||||
|
const temporalNamedOverride = winner.kind === 'temporal' ? sundayNamedOverrides[winner.id] : undefined;
|
||||||
|
const hasTemporalNamedOverride =
|
||||||
|
temporalNamedOverride !== undefined && (temporalNamedOverride.weekdays === undefined || temporalNamedOverride.weekdays.includes(day.weekday));
|
||||||
|
const threeNocturns =
|
||||||
|
day.weekday === 'sunday' || (winner.kind === 'sanctoral' && isAtLeast(winner.rank, 'semiduplex')) || hasTemporalNamedOverride;
|
||||||
const pool = buildReadingPool(day, temporalId, date, threeNocturns);
|
const pool = buildReadingPool(day, temporalId, date, threeNocturns);
|
||||||
const [nocturn1Readings, nocturn2Readings, nocturn3Readings] = distributeIntoNocturns(pool, threeNocturns ? 3 : 1);
|
const [nocturn1Readings, nocturn2Readings, nocturn3Readings] = distributeIntoNocturns(pool, threeNocturns ? 3 : 1);
|
||||||
|
|
||||||
@@ -767,7 +790,7 @@ export function resolveOrdo(date: string): ResolvedOrdo {
|
|||||||
{ kind: 'hymn', text: resolveMatinsHymn(day) },
|
{ kind: 'hymn', text: resolveMatinsHymn(day) },
|
||||||
];
|
];
|
||||||
|
|
||||||
if (threeNocturns && day.weekday === 'sunday') {
|
if (threeNocturns && (day.weekday === 'sunday' || hasTemporalNamedOverride)) {
|
||||||
parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[0]}` });
|
parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[0]}` });
|
||||||
parts.push(...sundayPsalmNocturn(sundayNocturnFor('nocturn1', day, temporalId), day));
|
parts.push(...sundayPsalmNocturn(sundayNocturnFor('nocturn1', day, temporalId), day));
|
||||||
parts.push(...(nocturn1Readings ?? []));
|
parts.push(...(nocturn1Readings ?? []));
|
||||||
|
|||||||
Reference in New Issue
Block a user