Fix Matins nocturn readings leaking suppressed Sunday content

nocturnReadingIds unconditionally pooled the plain temporal-cycle and
month-week reading ids into every day's nocturn-reading pool, even when
a real feast wins the day outright with zero commemorations
(decideOccurrence's `ordinary-feria` branch, which correctly suppresses
the temporal identity entirely). St. Bartholomew (duplex-2-classis,
2026-08-24, a Monday) has no Nocturn 3 content of his own, so the
leftover 13th-Sunday-after-Pentecost and month-week readings wrongly
filled his Nocturn 3.

Gate the pooling on whether the day's own occurrence decision actually
retained the temporal identity (day.winner.kind === 'temporal', or a
`temporal`-kind entry in day.commemorations) — mirrors getDayCollects's
existing pattern for the same question.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F25189JqjXddUhU9hM9nUS
This commit is contained in:
2026-08-24 07:11:09 -04:00
parent 42dd4dc04b
commit f8de318b7f
2 changed files with 53 additions and 3 deletions
+18 -3
View File
@@ -353,9 +353,24 @@ function nocturnReadingIds(day: LiturgicalDay, temporalId: string, date: string)
// nocturn-readings content too, not just a commemorated sanctoral one. // nocturn-readings content too, not just a commemorated sanctoral one.
if (c.kind === 'sanctoral' || c.kind === 'temporal') ids.add(c.id); if (c.kind === 'sanctoral' || c.kind === 'temporal') ids.add(c.id);
} }
ids.add(temporalId); // The plain temporalId/month-week content is only pooled when the day's
const monthWeek = monthWeekId(date); // own occurrence decision (calendar/commemorations.ts's decideOccurrence)
if (monthWeek) ids.add(`month-week-${monthWeek}`); // actually retained the temporal identity in some form: the temporal
// cycle won outright (day.winner.kind === 'temporal' -- a plain ferial/
// Sunday, or a named temporal override like Christ the King), or it
// survives as a commemoration alongside a sanctoral winner. Excluded:
// decideOccurrence's `ordinary-feria` branch, where a real feast --
// however low-ranked -- wins with zero commemorations, correctly
// suppressing the temporal identity entirely (e.g. St. Bartholomew,
// duplex-2-classis, 2026-08-24 -- his own proper reading has no Nocturn
// 3 content, and without this gate the leftover 13th-Sunday-after-
// Pentecost/month-week content wrongly filled Nocturn 3 instead).
const temporalKept = day.winner.kind === 'temporal' || day.commemorations.some((c) => c.kind === 'temporal');
if (temporalKept) {
ids.add(temporalId);
const monthWeek = monthWeekId(date);
if (monthWeek) ids.add(`month-week-${monthWeek}`);
}
return [...ids]; return [...ids];
} }
+35
View File
@@ -490,3 +490,38 @@ describe('resolveOrdo("matins", ...) calendar-month/week nocturn-readings import
} }
}); });
}); });
// Real bug (2026-08-24 fix): nocturnReadingIds unconditionally pooled the
// plain temporalId/month-week ids into every day's nocturn-reading pool,
// even on a day where a real feast wins outright with zero commemorations
// (decideOccurrence's `ordinary-feria` branch) -- correctly suppressing the
// temporal identity entirely. St. Bartholomew (duplex-2-classis, 2026-08-24,
// a Monday) has no Nocturn 3 content of his own, so the leftover 13th-
// Sunday-after-Pentecost (post-pentecost-13.yml) and month-week (month-
// week-084.yml) readings wrongly filled his Nocturn 3. Fixed by gating that
// pooling on whether the day's own occurrence decision actually retained
// the temporal identity (day.winner.kind === 'temporal', or a `kind:
// 'temporal'` entry in day.commemorations).
describe('resolveOrdo("matins", ...) nocturn-reading temporal/month-week suppression (2026-08-24 fix)', () => {
function lessonLabels(date: string) {
const ordo = resolveOrdo('matins', date);
return ordo.parts.filter((p) => p.kind === 'lesson').map((p) => (p as { label?: string }).label);
}
it("St. Bartholomew (2026-08-24, duplex-2-classis, zero commemorations) does not pool the suppressed Sunday/month-week readings", () => {
const labels = lessonLabels('2026-08-24');
expect(labels).not.toContain('St. Augustine, Bishop of Hippo, Book 2, Questions on the Gospels, ch. 40');
expect(labels).not.toContain('St. Gregory the Great, Moralia in Job, Book 1, ch. 10');
});
it("St. Bartholomew's own Nocturn 2 still carries his own proper vita reading", () => {
const ordo = resolveOrdo('matins', '2026-08-24');
const lessons = ordo.parts.filter((p) => p.kind === 'lesson') as { text: { text: Record<string, string> } }[];
expect(lessons.some((l) => l.text.text.la?.includes('Bartholomǽus Apóstolus'))).toBe(true);
});
it('a plain ferial weekday with no sanctoral winner (2026-09-04, a gap day in sanctoral-calendar.yml) still pools the plain temporal/month-week readings, unaffected by the gate', () => {
const ordo = resolveOrdo('matins', '2026-09-04');
const lessons = ordo.parts.filter((p) => p.kind === 'lesson') as { text: { status: Record<string, string> } }[];
expect(lessons.length).toBeGreaterThan(0);
expect(lessons.some((l) => l.text.status.la !== 'missing' && l.text.status.en !== 'missing')).toBe(true);