Fix Matins hymn to actually keep the Assumption's octave hymn all week
Deploy / deploy (push) Successful in 1m31s
Deploy / deploy (push) Successful in 1m31s
resolveMatinsHymn's own doc comment already claimed the octave "keeps the feast's own proper hymn all week," but the code never implemented it -- it only matched when the day's own winner literally was `assumption` (Aug 15 itself). Every other octave day (16, 17, 19, 20, 21, none of whom has a Matins hymn of their own authored) fell straight through to the plain ferial hymn instead. Adds a real octave-fallback tier between the existing per-feast override and the seasonal tier. A deliberate departure from the reference engine, which doesn't do this either (Monastic 1617's octave days have no [Hymnus Matutinum] override at all) -- consistent with how this project already treats octave readings/commemorations more generously than any one source track. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VZSAgRi4QE4XRTqVto93zA
This commit is contained in:
@@ -2296,6 +2296,38 @@ two years' Easter dates, and `tests/calendar/august-sanctoral.test.ts`'s existin
|
|||||||
was updated to match (the day now reverts to the plain Assumption octave, same shape as St.
|
was updated to match (the day now reverts to the plain Assumption octave, same shape as St.
|
||||||
Lawrence's own Aug 12 case). `npm test` (429 tests) and `tsc --noEmit` both pass.
|
Lawrence's own Aug 12 case). `npm test` (429 tests) and `tsc --noEmit` both pass.
|
||||||
|
|
||||||
|
### Assumption-octave Matins hymn fixed (2026-08-22)
|
||||||
|
|
||||||
|
Separately, the "no antiphons and a weird hymn today" complaint that prompted the investigation
|
||||||
|
above turned out to be a distinct, narrower bug: `resolveMatinsHymn`'s own doc comment
|
||||||
|
already claimed the Assumption's octave "keeps the feast's own proper hymn all week," but the
|
||||||
|
code never implemented that — it only fired when the day's own winner literally *was*
|
||||||
|
`assumption` (Aug 15 only). Every other octave day (16, 17, 19, 20, 21 — St. Joachim, St.
|
||||||
|
Hyacinth, St. John Eudes, St. Bernard, St. Jane Frances de Chantal, none of whom has a Matins
|
||||||
|
hymn of their own authored) fell straight to the plain ferial hymn. Fixed by adding a real
|
||||||
|
octave-fallback tier (`resolveActiveOctave(day.date)` → `matins-hymn-${octave.id}`) between the
|
||||||
|
existing per-feast override and the seasonal tier. Confirmed against the reference engine that
|
||||||
|
Monastic 1617 itself doesn't do this either (no octave day there has its own `[Hymnus
|
||||||
|
Matutinum]` override) — this is a deliberate departure, not a restoration, consistent with how
|
||||||
|
this project already treats octave readings/commemorations more generously than any one source
|
||||||
|
track. Aug 18 (no sanctoral winner) and Aug 22 (once IHM moved) needed no change — both already
|
||||||
|
reached `matins-hymn-assumption` via the pre-existing `resolveOfficeWinner` octave-substitution
|
||||||
|
path, which only fires when the day's own winner isn't sanctoral.
|
||||||
|
|
||||||
|
**Not fixed, logged instead** (user decision): the deeper reason Matins had *zero* nocturn
|
||||||
|
antiphons at all on Aug 22 — no `matins-psalmody-overrides` entry exists for `assumption` or
|
||||||
|
`immaculate-heart-of-mary` (or, generally, any Duplex-2-classis+ feast lacking one), so Matins
|
||||||
|
falls all the way to the bare ferial psalm table, which by design carries no antiphons — is a
|
||||||
|
broader content gap than today's date, tracked in the "Matins psalmody overrides" bullet above
|
||||||
|
rather than authored now. Also newly true because of this change: `hours/matins.ts`'s psalmody
|
||||||
|
override lookup is hard-gated to `winner.kind === 'sanctoral'`, so even if IHM's psalmody were
|
||||||
|
authored later, it wouldn't apply as a temporal-kind winner without that gate being loosened
|
||||||
|
too — noted for whoever picks this up.
|
||||||
|
|
||||||
|
New tests in `tests/hours/matins.test.ts` cover the fallback across Aug 16-22, including the
|
||||||
|
Aug 17 edge case (correctly still ferial, since that day is actually governed by St. Lawrence's
|
||||||
|
own octave, not the Assumption's). `npm test` (430 tests) and `tsc --noEmit` both pass.
|
||||||
|
|
||||||
## Known, deliberate simplifications (not bugs — working as designed)
|
## Known, deliberate simplifications (not bugs — working as designed)
|
||||||
|
|
||||||
- `getDayCollects`: each collect in a multi-collect day renders as its own
|
- `getDayCollects`: each collect in a multi-collect day renders as its own
|
||||||
|
|||||||
+29
-8
@@ -62,7 +62,7 @@
|
|||||||
// full-calendar content pass. See TODO.md for what's deferred.
|
// full-calendar content pass. See TODO.md for what's deferred.
|
||||||
import type { ResolvedOrdo, ResolvedPart, ResolvedText, ResolvedVerse } from './types';
|
import type { ResolvedOrdo, ResolvedPart, ResolvedText, ResolvedVerse } from './types';
|
||||||
import type { LiturgicalDay } from '../calendar/types';
|
import type { LiturgicalDay } from '../calendar/types';
|
||||||
import { resolveDay, resolveTemporalId, activeOctavesFor } from '../calendar';
|
import { resolveDay, resolveTemporalId, activeOctavesFor, resolveActiveOctave } from '../calendar';
|
||||||
import { isInTriduum } from '../calendar/temporal';
|
import { isInTriduum } from '../calendar/temporal';
|
||||||
import { getDayLabel } from '../calendar/day-label';
|
import { getDayLabel } from '../calendar/day-label';
|
||||||
import { getPsalmVerses } from '../psalter';
|
import { getPsalmVerses } from '../psalter';
|
||||||
@@ -215,14 +215,28 @@ function sundayCanticleNocturn(group: SundayNocturn, day: LiturgicalDay): Resolv
|
|||||||
/** The Matins hymn — a duplex-majus+ saint's or eligible named temporal
|
/** The Matins hymn — a duplex-majus+ saint's or eligible named temporal
|
||||||
* feast's own proper hymn (`matins-hymn-${overrideId}`, via the same
|
* feast's own proper hymn (`matins-hymn-${overrideId}`, via the same
|
||||||
* getOfficeOverrideId eligibility Lauds/Vespers' own resolveOffice uses),
|
* getOfficeOverrideId eligibility Lauds/Vespers' own resolveOffice uses),
|
||||||
* when authored, else falls to a *seasonal* default (Advent/Lent/
|
* when authored; else, on a day within an active octave whose own feast
|
||||||
|
* has a hymn authored, that octave's hymn (`matins-hymn-${octave.id}`) —
|
||||||
|
* see below; else falls to a *seasonal* default (Advent/Lent/
|
||||||
* Passiontide/Paschaltide, none authored yet), else the plain year-round
|
* Passiontide/Paschaltide, none authored yet), else the plain year-round
|
||||||
* ferial hymn — same override > season > ferial precedence every other
|
* ferial hymn — same override > octave > season > ferial precedence every
|
||||||
* hour's own office bundle uses (see lauds.ts's resolveOffice), just never
|
* other hour's own office bundle uses (see lauds.ts's resolveOffice), just
|
||||||
* wired up here before. Concrete motivating case (user, 2026-08-21): the
|
* never fully wired up here before.
|
||||||
* 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
|
* Concrete motivating case (user, 2026-08-21/22): the Assumption's octave
|
||||||
* ferial one this always rendered previously. */
|
* (`Sancti/08-21bmv.txt`'s own `[Rule] ex Sancti/08-15`) genuinely keeps
|
||||||
|
* the feast's own proper hymn all week — a claim this comment already
|
||||||
|
* made before the octave tier below actually existed, which only ever
|
||||||
|
* fired on Aug 15 itself (the one day `overrideId` literally *is*
|
||||||
|
* `assumption`). Every other octave day (16, 17, 19, 20, 21, each with
|
||||||
|
* its own named saint who has no Matins hymn of their own authored) fell
|
||||||
|
* straight through to the plain ferial hymn instead. This is a deliberate
|
||||||
|
* departure from the reference engine, which doesn't do this either
|
||||||
|
* (Monastic 1617's own octave days have no `[Hymnus Matutinum]` override
|
||||||
|
* at all, live-checked 2026-08-22) — not a restoration of source
|
||||||
|
* behavior, just this project's own generous octave design (see
|
||||||
|
* "Not a reconstruction" in the repo's CLAUDE.md) applied to the hymn the
|
||||||
|
* same way it's already applied to readings/commemorations elsewhere. */
|
||||||
function resolveMatinsHymn(day: LiturgicalDay): ResolvedText {
|
function resolveMatinsHymn(day: LiturgicalDay): ResolvedText {
|
||||||
const overrideId = getOfficeOverrideId(day);
|
const overrideId = getOfficeOverrideId(day);
|
||||||
if (overrideId) {
|
if (overrideId) {
|
||||||
@@ -231,6 +245,13 @@ function resolveMatinsHymn(day: LiturgicalDay): ResolvedText {
|
|||||||
return proper;
|
return proper;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const octave = resolveActiveOctave(day.date);
|
||||||
|
if (octave) {
|
||||||
|
const octaveHymn = resolveCommon(`matins-hymn-${octave.id}`);
|
||||||
|
if (octaveHymn.status.la !== 'missing' || octaveHymn.status.en !== 'missing') {
|
||||||
|
return octaveHymn;
|
||||||
|
}
|
||||||
|
}
|
||||||
const seasonSuffix = seasonalOfficeSuffix(day.season);
|
const seasonSuffix = seasonalOfficeSuffix(day.season);
|
||||||
if (seasonSuffix) {
|
if (seasonSuffix) {
|
||||||
const seasonal = resolveCommon(`matins-hymn-${seasonSuffix}`);
|
const seasonal = resolveCommon(`matins-hymn-${seasonSuffix}`);
|
||||||
|
|||||||
@@ -371,3 +371,39 @@ describe('resolveOrdo("matins", ...) Sacred Triduum', () => {
|
|||||||
expect(ordo.parts[1]).toMatchObject({ kind: 'psalm', psalmNumber: 3 });
|
expect(ordo.parts[1]).toMatchObject({ kind: 'psalm', psalmNumber: 3 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('resolveOrdo("matins", ...) Assumption-octave hymn fallback (2026-08-22 fix)', () => {
|
||||||
|
// Real bug: resolveMatinsHymn's own doc comment already claimed the
|
||||||
|
// Assumption's octave "keeps the feast's own proper hymn all week," but
|
||||||
|
// the code only ever matched when the day's own winner literally *was*
|
||||||
|
// `assumption` (Aug 15 itself) -- every other octave day fell straight
|
||||||
|
// through to the plain ferial hymn instead. Fixed by adding a real
|
||||||
|
// octave-fallback tier (resolveActiveOctave -> `matins-hymn-${octave.id}`).
|
||||||
|
function hymnLatin(date: string): string | undefined {
|
||||||
|
const ordo = resolveOrdo('matins', date);
|
||||||
|
const hymn = ordo.parts.find((p) => p.kind === 'hymn') as { text: { text: { la: string } } } | undefined;
|
||||||
|
return hymn?.text.text.la;
|
||||||
|
}
|
||||||
|
|
||||||
|
it("St. Bernard's own day (Aug 20, no hymn of his own authored) picks up the Assumption's octave hymn, not the plain ferial one", () => {
|
||||||
|
expect(hymnLatin('2026-08-20')).toContain('Surge');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('every other interior day governed by the Assumption octave (16, 19, 21 — none with a hymn of their own) also picks up the octave hymn', () => {
|
||||||
|
for (const date of ['2026-08-16', '2026-08-19', '2026-08-21']) {
|
||||||
|
expect(hymnLatin(date)).toContain('Surge');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Aug 17 is instead governed by St. Lawrence's own octave (its own elevated closing day, outranking the Assumption's ordinary day here) — no hymn authored for it, so it still falls to the plain ferial one, correctly", () => {
|
||||||
|
expect(hymnLatin('2026-08-17')).toContain('Somno');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Aug 22, the octave-closing day (Immaculate Heart of Mary relocated away), also gets the Assumption hymn via the pre-existing sanctoral-substitution path', () => {
|
||||||
|
expect(hymnLatin('2026-08-22')).toContain('Surge');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("a plain ferial day outside the octave still gets the plain ferial hymn, unaffected", () => {
|
||||||
|
expect(hymnLatin(FERIAL_DATE)).toContain('Somno');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user