Add Matins hymn override mechanism

Matins always rendered the plain ferial hymn, unlike Lauds/Vespers which
already check a duplex-majus+ override then a seasonal default first.
Surfaced by the Assumption's octave, which keeps the feast's own proper
hymn all week rather than falling back to ferial.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3wxvz3mPXiHxkPB5JpnmD
This commit is contained in:
2026-08-21 07:56:06 -04:00
parent 4835b35fc1
commit 62c74539f8
+40 -3
View File
@@ -60,7 +60,7 @@
// Only a small, growable slice of content is authored so far (one clean
// ferial day, one clean Sunday) — this is the mechanism build, not the
// full-calendar content pass. See TODO.md for what's deferred.
import type { ResolvedOrdo, ResolvedPart, ResolvedVerse } from './types';
import type { ResolvedOrdo, ResolvedPart, ResolvedText, ResolvedVerse } from './types';
import type { LiturgicalDay } from '../calendar/types';
import { resolveDay, resolveTemporalId, activeOctavesFor } from '../calendar';
import { isInTriduum } from '../calendar/temporal';
@@ -70,7 +70,15 @@ import { getPsalmsFor, type PsalmRef } from '../psalter/distribution';
import { getScriptureVerses } from '../scripture';
import { getOpeningVersicleId } from './opening-versicle';
import { isDoubleOrHigher } from './antiphon';
import { resolveCommon, getDayCollect, resolveOfficeWinner, verifiedText, splitNamedAntiphon } from './resolve-common';
import {
resolveCommon,
getDayCollect,
getOfficeOverrideId,
resolveOfficeWinner,
seasonalOfficeSuffix,
verifiedText,
splitNamedAntiphon,
} from './resolve-common';
import { getBiblePlanReadings } from '../propers/bible-plan';
import { getNocturnReadings, type NocturnReading } from '../propers/nocturn-readings';
import { getOctaveReading } from '../propers/octave-readings';
@@ -180,6 +188,35 @@ function sundayCanticleNocturn(group: SundayNocturn, day: LiturgicalDay): Resolv
return parts;
}
/** The Matins hymn — a duplex-majus+ saint's or eligible named temporal
* feast's own proper hymn (`matins-hymn-${overrideId}`, via the same
* getOfficeOverrideId eligibility Lauds/Vespers' own resolveOffice uses),
* when authored, else falls to a *seasonal* default (Advent/Lent/
* Passiontide/Paschaltide, none authored yet), else the plain year-round
* ferial hymn — same override > season > ferial precedence every other
* hour's own office bundle uses (see lauds.ts's resolveOffice), just never
* wired up here before. Concrete motivating case (user, 2026-08-21): the
* Assumption's octave (`Sancti/08-21bmv.txt`'s own `[Rule] ex Sancti/
* 08-15`) genuinely keeps the feast's own proper hymn all week, not the
* ferial one this always rendered previously. */
function resolveMatinsHymn(day: LiturgicalDay): ResolvedText {
const overrideId = getOfficeOverrideId(day);
if (overrideId) {
const proper = resolveCommon(`matins-hymn-${overrideId}`);
if (proper.status.la !== 'missing' || proper.status.en !== 'missing') {
return proper;
}
}
const seasonSuffix = seasonalOfficeSuffix(day.season);
if (seasonSuffix) {
const seasonal = resolveCommon(`matins-hymn-${seasonSuffix}`);
if (seasonal.status.la !== 'missing' || seasonal.status.en !== 'missing') {
return seasonal;
}
}
return resolveCommon('matins-hymn-ferial');
}
function ferialPsalmody(day: LiturgicalDay): ResolvedPart[] {
return psalmRefParts(getPsalmsFor('matins', day.weekday));
}
@@ -317,7 +354,7 @@ export function resolveOrdo(date: string): ResolvedOrdo {
plainPsalm(3),
{ kind: 'section-heading', label: 'Invitatory' },
...invitatoryParts(day),
{ kind: 'hymn', text: resolveCommon('matins-hymn-ferial') },
{ kind: 'hymn', text: resolveMatinsHymn(day) },
];
if (threeNocturns && day.weekday === 'sunday') {