Name Ember days in the day label, demoted or winning outright

calendar/day-label.ts never surfaced an Ember day's own identity
anywhere: collectCommemorations only matched a temporal commemoration
against resolveTemporalId(day.date), never an Ember-relabeled id, and
the winning-outright fallback used the generic ordinal weekday label
with no id awareness at all. Adds a standalone emberDayLabel lookup,
deliberately kept out of temporal-feasts.ts since that table doubles
as isFerialOrVigil's bareness check and would have silently turned
off Ember days' own ferial Preces.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
This commit is contained in:
2026-09-04 07:12:32 -04:00
parent 03d39dc911
commit 88e8a5b404
3 changed files with 74 additions and 1 deletions
+20
View File
@@ -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.
+38 -1
View File
@@ -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<string, Bi> = {
'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<Record<string, string>>
}
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]));
+16
View File
@@ -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) {