Files
vu/src/calendar/ember-days.ts
T
will 9fb9cb5e24
Deploy / deploy (push) Successful in 1m17s
Add Ember-day mechanism (privileged feria, not a rank contest) + real content
Researched whether September/Advent Ember days belong in the
movable-feasts.ts table alongside Christ the King/Immaculate Heart of
Mary. Live-verified against the reference engine they don't: Ember days
are privileged ferias (a saint can still win outright against them), not
named feasts contesting the day by rank.

Two smaller pieces instead: calendar/temporal.ts's septemberEmberDayOffset
(Sunday nearest Sept 14, same "nearest" arithmetic adventStart already
used, factored into a shared nearestSunday helper) feeds a new
resolveTemporalCategory check giving those 3 dates privileged-feria-minor
(live-verified correct); calendar/ember-days.ts's applyEmberDay relabels
the day's own temporal id to the Ember day's own (so its real content is
found) when the feria itself wins, or adds a commemoration when a saint
does. Advent Ember days needed no precedence change -- Advent's own
season default already covers them.

Found and fixed a real bug along the way: matins.ts's nocturnReadingIds
only ever pulled a commemorated *sanctoral* id's own readings, never a
commemorated *temporal* one's -- so a commemorated Ember day (the common
case) would never have surfaced its own content. Same root cause as the
day.winner.id gap the IHM relocation fixed earlier, just hiding in the
commemorations loop instead.

Authored all 6 days' real collect + 3 Matins readings each, transcribed
directly from the reference engine (September: Tempora/093-{3,5,6}.txt;
Advent: Tempora/Adv3-{3,5,6}.txt, whose lessons are themselves a
cross-reference to the Annunciation's own Common, followed and
transcribed from there). Kept as 3 separate readings per day rather than
the usual combine-into-one default, since each carries its own
genuinely distinct proper responsory.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZSAgRi4QE4XRTqVto93zA
2026-08-22 07:27:52 -04:00

73 lines
3.1 KiB
TypeScript

// Ember days (Quatuor Temporum) are privileged ferias, not named feasts —
// structurally different from calendar/movable-feasts.ts's rank-contest
// table. The precedence side (whether an occurring saint outranks the
// feria) is already handled generically by resolveTemporalCategory
// (September Ember days get their own `privileged-feria-minor` entry
// there; Advent Ember days inherit Advent's own season default) feeding
// the ordinary decideOccurrence contest -- this file ONLY handles content
// identity: relabeling the day's own temporal id so an Ember day's real,
// distinct proper content (collect, Matins readings) gets found instead
// of inheriting whatever Sunday governs that week, the same way
// applyMarianSaturday/applyChristmasOctaveSunday relabel a feria's own id
// for the same reason.
import type { Commemoration, DayWinner } from './types';
import { septemberEmberDayOffset, adventEmberDayOffset } from './temporal';
import { activeOctavesFor } from './octaves';
const SEPTEMBER_EMBER_IDS: Record<number, string> = {
3: 'ember-september-wednesday',
5: 'ember-september-friday',
6: 'ember-september-saturday',
};
const ADVENT_EMBER_IDS: Record<number, string> = {
3: 'ember-advent-wednesday',
5: 'ember-advent-friday',
6: 'ember-advent-saturday',
};
function emberIdFor(isoDate: string): string | undefined {
const septemberOffset = septemberEmberDayOffset(isoDate);
if (septemberOffset !== undefined) {
return SEPTEMBER_EMBER_IDS[septemberOffset];
}
const adventOffset = adventEmberDayOffset(isoDate);
if (adventOffset !== undefined) {
return ADVENT_EMBER_IDS[adventOffset];
}
return undefined;
}
/**
* Run after applyOctaves, same gate that function's own doc comment and
* applyMarianSaturday both rely on: an active octave (the Nativity of the
* BVM's, Sep 8-15, can genuinely overlap a late-anchored September Ember
* week in some years) already has real standing of its own and shouldn't
* be silently relabeled out from under it.
*
* Two cases, mirroring how a "Commemoratio ad Laudes tantum" reads
* live against the reference engine: if the feria itself is what's
* winning (`winner.kind === 'temporal'`, i.e. resolveTemporalCategory's
* `privileged-feria-minor` threshold already beat whatever saint was
* there, or nothing was there at all), its id is swapped for the Ember
* day's own — real content gets found where it wouldn't otherwise be. If
* a saint won outright instead (strong enough to clear that threshold),
* the winner is left alone but the Ember day is still commemorated
* alongside it, so its own collect/readings still surface (this app
* doesn't model the "Laudes only" hour-scoping nuance — same simplification
* level as every other commemoration here, not a new gap).
*/
export function applyEmberDay(isoDate: string, winner: DayWinner, commemorations: Commemoration[]): DayWinner {
if (activeOctavesFor(isoDate).length > 0) {
return winner;
}
const emberId = emberIdFor(isoDate);
if (!emberId) {
return winner;
}
if (winner.kind === 'temporal') {
return { kind: 'temporal', id: emberId };
}
commemorations.push({ kind: 'temporal', id: emberId });
return winner;
}