diff --git a/TODO.md b/TODO.md index 05b33d0..5eaf390 100644 --- a/TODO.md +++ b/TODO.md @@ -5232,3 +5232,23 @@ all 6 winning-outright dates; Magnificat status on Advent Ember Wednesday, the o overriding mechanism to complicate the assertion). `npm test` (2023 passed, 47 files) and `tsc --noEmit` both pass. +**Follow-on, same day**: user asked why the Ember days didn't actually show anywhere in the app +on 2026-09-16/18/19 despite the above. Real bug, found by resolving `getDayLabel` directly for +those dates: `calendar/day-label.ts`'s `collectCommemorations` only ever matched a `temporal`-kind +commemoration against `resolveTemporalId(day.date)` (the plain, un-relabeled id for that date) — +never against an Ember-relabeled id, so `applyEmberDay`'s own commemoration was silently dropped +from the label every time. Worse, even the *winning-outright* case had no name: the final +fallback branch calls the generic `temporalLabel(day)` (pure season/weekday arithmetic, no id +awareness at all), so an Ember day that won outright rendered as bare "Wednesday in the Nth week +after Trinity (Feria)" with zero mention of Ember anywhere. + +Fixed with a small standalone `EMBER_DAY_NAMES`/`emberDayLabel` lookup in `day-label.ts` itself +(6 entries, Latin names read straight from each day's own `[Officium]` header) — deliberately +*not* added to `temporal-feasts.ts`'s named-feast table, since that table doubles as `resolve- +common.ts`'s `isFerialOrVigil` bareness check (`isBareFeria = ... && !getTemporalFeastRecord(...)`) +and adding Ember ids there would have silently turned off their own ferial Preces (their `[Rule]` +is literally "Preces Feriales"). Wired into both places: `collectCommemorations`'s temporal +branch (demoted case) and `getDayLabel`'s final fallback (winning-outright case). New assertions +in `tests/calendar/ember-days.test.ts` cover both. `npm test` (2024 passed) and `tsc --noEmit` +both pass. + diff --git a/src/calendar/day-label.ts b/src/calendar/day-label.ts index 15f8c05..4c01d9c 100644 --- a/src/calendar/day-label.ts +++ b/src/calendar/day-label.ts @@ -50,6 +50,38 @@ function joinBi(items: Bi[], sep = ' — '): Bi { return { en: items.map((i) => i.en).join(sep), la: items.map((i) => i.la).join(sep) }; } +/** + * The 6 Ember-day ids (calendar/ember-days.ts) are deliberately *not* in + * temporal-feasts.ts's named-feast table — they're privileged ferias, not + * rank-contesting movable feasts (see that file's own doc comment), and + * that table's `getTemporalFeastRecord` also doubles as resolve- + * common.ts's `isFerialOrVigil` bareness check, so adding them there + * would silently turn off their own ferial Preces (their `[Rule]` block + * is literally "Preces Feriales"). A small standalone lookup here instead + * — Latin names read straight from each day's own `[Officium]` header + * (Tempora/093-{3,5,6}.txt, Tempora/Adv3-{3,5,6}.txt). + * + * Without this, an Ember day never named itself anywhere in the UI: not + * as the primary label when it wins outright (fell through to the plain + * "Wednesday in the Nth week after Trinity"/"In the Nth week of Advent" + * wording with no Ember mention at all), and not as a commemoration when + * demoted (collectCommemorations's temporal branch only ever matched + * `resolveTemporalId(day.date)`, never an Ember-relabeled id) — live- + * verified as a real, user-visible gap, 2026-09-03. + */ +const EMBER_DAY_NAMES: Record = { + 'ember-september-wednesday': bi('Ember Wednesday in September', 'Feria Quarta Quattuor Temporum Septembris'), + 'ember-september-friday': bi('Ember Friday in September', 'Feria Sexta Quattuor Temporum Septembris'), + 'ember-september-saturday': bi('Ember Saturday in September', 'Sabbato Quattuor Temporum Septembris'), + 'ember-advent-wednesday': bi('Ember Wednesday in Advent', 'Feria IV Quattuor Temporum in Adventu'), + 'ember-advent-friday': bi('Ember Friday in Advent', 'Feria VI Quattuor Temporum in Adventu'), + 'ember-advent-saturday': bi('Ember Saturday in Advent', 'Sabbato Quattuor Temporum in Adventu'), +}; + +function emberDayLabel(id: string): Bi | undefined { + return EMBER_DAY_NAMES[id]; +} + function capitalize(text: string): string { return text.charAt(0).toUpperCase() + text.slice(1); } @@ -458,6 +490,11 @@ function collectCommemorations( } else if (c.kind === 'temporal') { if (c.id === temporalId) { temporal.push(anchorDayName(day, false) ?? temporalLabel(day)); + } else { + const emberName = emberDayLabel(c.id); + if (emberName) { + temporal.push(emberName); + } } } else if (opts.showOctaves && c.id !== opts.excludeOctaveId) { const octave = activeOctaves.find((a) => a.id === c.id); @@ -663,7 +700,7 @@ export function getDayLabel(day: LiturgicalDay): Partial> } const temporalRank = TEMPORAL_CATEGORY_RANK_LABELS[day.temporalCategory]; - const label = temporalLabel(day); + const label = (day.winner.kind === 'temporal' ? emberDayLabel(day.winner.id) : undefined) ?? temporalLabel(day); const temporal = bi(`${label.en} (${temporalRank.en})`, `${label.la} (${temporalRank.la})`); const { sanctoral } = collectCommemorations(day, { showOctaves: false }); return withTransferNote(day, joinBi([temporal, ...sanctoral])); diff --git a/tests/calendar/ember-days.test.ts b/tests/calendar/ember-days.test.ts index b642af8..d14d358 100644 --- a/tests/calendar/ember-days.test.ts +++ b/tests/calendar/ember-days.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import { resolveDay } from '../../src/calendar'; +import { getDayLabel } from '../../src/calendar/day-label'; import { resolveOrdo } from '../../src/hours'; import { getDayCollect } from '../../src/hours/resolve-common'; @@ -89,6 +90,21 @@ describe('Ember days', () => { expect(antiphon.text.status.en).toBe('verified'); }); + it("the day label names the Ember day itself, both when it's demoted to a commemoration and when it wins outright -- previously never shown anywhere in the UI (2026-09-03 fix)", () => { + // Demoted cases (2026): a saint wins outright, Ember rides along as a + // commemoration -- collectCommemorations' temporal branch previously + // only matched resolveTemporalId(day.date), never an Ember-relabeled id. + expect(getDayLabel(resolveDay('2026-09-16')).en).toContain('Ember Wednesday in September'); + expect(getDayLabel(resolveDay('2026-09-18')).en).toContain('Ember Friday in September'); + expect(getDayLabel(resolveDay('2026-09-19')).en).toContain('Ember Saturday in September'); + // Winning-outright cases: previously fell through to the plain + // ordinal weekday label with no Ember mention at all. + expect(getDayLabel(resolveDay('2023-09-20')).en).toBe('Ember Wednesday in September (Feria) — Vigil of St. Matthew'); + expect(getDayLabel(resolveDay('2026-12-16')).en).toBe('Ember Wednesday in Advent (Feria)'); + expect(getDayLabel(resolveDay('2026-12-18')).en).toBe('Ember Friday in Advent (Feria)'); + expect(getDayLabel(resolveDay('2026-12-19')).en).toContain('Ember Saturday in Advent (Feria)'); + }); + it('Matins renders 3 real, distinct, verified lessons+responsories for each of the 6 Ember days', () => { const dates = ['2026-09-16', '2026-09-18', '2026-09-19', '2026-12-16', '2026-12-18', '2026-12-19']; for (const date of dates) {