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:
@@ -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
|
overriding mechanism to complicate the assertion). `npm test` (2023 passed, 47 files) and
|
||||||
`tsc --noEmit` both pass.
|
`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.
|
||||||
|
|
||||||
|
|||||||
@@ -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) };
|
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 {
|
function capitalize(text: string): string {
|
||||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||||
}
|
}
|
||||||
@@ -458,6 +490,11 @@ function collectCommemorations(
|
|||||||
} else if (c.kind === 'temporal') {
|
} else if (c.kind === 'temporal') {
|
||||||
if (c.id === temporalId) {
|
if (c.id === temporalId) {
|
||||||
temporal.push(anchorDayName(day, false) ?? temporalLabel(day));
|
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) {
|
} else if (opts.showOctaves && c.id !== opts.excludeOctaveId) {
|
||||||
const octave = activeOctaves.find((a) => a.id === c.id);
|
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 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 temporal = bi(`${label.en} (${temporalRank.en})`, `${label.la} (${temporalRank.la})`);
|
||||||
const { sanctoral } = collectCommemorations(day, { showOctaves: false });
|
const { sanctoral } = collectCommemorations(day, { showOctaves: false });
|
||||||
return withTransferNote(day, joinBi([temporal, ...sanctoral]));
|
return withTransferNote(day, joinBi([temporal, ...sanctoral]));
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { resolveDay } from '../../src/calendar';
|
import { resolveDay } from '../../src/calendar';
|
||||||
|
import { getDayLabel } from '../../src/calendar/day-label';
|
||||||
import { resolveOrdo } from '../../src/hours';
|
import { resolveOrdo } from '../../src/hours';
|
||||||
import { getDayCollect } from '../../src/hours/resolve-common';
|
import { getDayCollect } from '../../src/hours/resolve-common';
|
||||||
|
|
||||||
@@ -89,6 +90,21 @@ describe('Ember days', () => {
|
|||||||
expect(antiphon.text.status.en).toBe('verified');
|
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', () => {
|
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'];
|
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) {
|
for (const date of dates) {
|
||||||
|
|||||||
Reference in New Issue
Block a user