From cf4edfae2bad9225e64bc37615a00d1a8d2b5938 Mon Sep 17 00:00:00 2001 From: Will Estes Date: Sat, 22 Aug 2026 06:00:42 -0400 Subject: [PATCH] Fix Matins hymn to actually keep the Assumption's octave hymn all week 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 Claude-Session: https://claude.ai/code/session_01VZSAgRi4QE4XRTqVto93zA --- TODO.md | 32 ++++++++++++++++++++++++++++++++ src/hours/matins.ts | 37 +++++++++++++++++++++++++++++-------- tests/hours/matins.test.ts | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 8 deletions(-) diff --git a/TODO.md b/TODO.md index ac53332..a70c594 100644 --- a/TODO.md +++ b/TODO.md @@ -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. 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) - `getDayCollects`: each collect in a multi-collect day renders as its own diff --git a/src/hours/matins.ts b/src/hours/matins.ts index bb39f9c..270fe4d 100644 --- a/src/hours/matins.ts +++ b/src/hours/matins.ts @@ -62,7 +62,7 @@ // full-calendar content pass. See TODO.md for what's deferred. import type { ResolvedOrdo, ResolvedPart, ResolvedText, ResolvedVerse } from './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 { getDayLabel } from '../calendar/day-label'; 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 * 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/ + * 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 - * 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. */ + * ferial hymn — same override > octave > season > ferial precedence every + * other hour's own office bundle uses (see lauds.ts's resolveOffice), just + * never fully wired up here before. + * + * Concrete motivating case (user, 2026-08-21/22): 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 — 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 { const overrideId = getOfficeOverrideId(day); if (overrideId) { @@ -231,6 +245,13 @@ function resolveMatinsHymn(day: LiturgicalDay): ResolvedText { 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); if (seasonSuffix) { const seasonal = resolveCommon(`matins-hymn-${seasonSuffix}`); diff --git a/tests/hours/matins.test.ts b/tests/hours/matins.test.ts index 2829913..b3ba122 100644 --- a/tests/hours/matins.test.ts +++ b/tests/hours/matins.test.ts @@ -371,3 +371,39 @@ describe('resolveOrdo("matins", ...) Sacred Triduum', () => { 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'); + }); +});