Files
vu/src/calendar/feasts.ts
T
will a6261f044a
Deploy / deploy (push) Successful in 1m32s
Add nameLa mechanism for bilingual header, proof set of 11 feasts + St. Lawrence
Threads an optional nameLa field through SanctoralIdentity/SaintRecord/
TemporalFeastRecord/ActiveOctave so day-label.ts can render real Latin
proper names instead of always falling back to English. Authors the
proof set: all 11 named temporal feasts plus St. Lawrence (chosen to
exercise the octave-headline and octave-commemoration code paths too).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwsZQMjALCvy7u9tFDWmuQ
2026-08-31 06:30:48 -04:00

167 lines
8.9 KiB
TypeScript

// Sanctoral occurrence resolution: given a date, which saint(s) are
// assigned via data/calendar/sanctoral-calendar.yml's day -> saint-id
// mapping, and their rank from data/calendar/saints/<id>.yml. See
// calendar/temporal.ts for the separate temporal-cycle resolution, and
// calendar/commemorations.ts for how a candidate returned here actually
// gets weighed against the day's temporal standing.
//
// Doesn't resolve clashes *among* multiple saints sharing a date — real
// practice has its own precedence/commemoration rules for that too, not
// modeled here. If a day has more than one candidate, calendar/index.ts
// just picks the highest-ranked as the day's sole sanctoral contender.
import type { FeastClass, OctaveConfig } from './types';
import sanctoralCalendarData from '../data/calendar/sanctoral-calendar.yml';
export interface SaintRecord {
id: string;
name: string;
/** Latin form of `name`, when authored — see calendar/types.ts's
* `SanctoralIdentity.nameLa` doc comment for the display fallback. */
nameLa?: string;
rank: FeastClass;
common: string;
propers: string | null;
/** Fallback id for Prime/Terce/Sext/None's antiphon/chapter when this
* saint has no proper file of their own for a given hour (see
* hours/resolve-common.ts's resolveMinorHourAntiphon/Chapter) — a
* *different* id than `common` above, deliberately: live-verified that
* the minor hours don't always draw from the same Common category as
* the saint's overall classification. E.g. both Chair of St. Peter
* feasts are `common: common-of-an-apostle` (Peter's own rank/collect
* classification), but their Prime/Terce/Sext/None content is drawn
* from the Common of a Confessor Bishop instead (Peter considered as
* bishop, for that specific commemoration) — `minorHoursCommon:
* common-of-a-confessor-bishop` captures that without corrupting the
* saint's real `common` field. Absent for saints with no shared-Common
* minor-hour content (either fully proper, or not yet authored).
*/
minorHoursCommon?: string;
/**
* Fallback id for the day collect (`getDayCollect`) when this saint has
* no proper collect of their own (`propers` may still be non-null —
* e.g. St. Agatha and St. Lucy both have a proper antiphon but a
* Common-sourced collect) — the *full* id of a `data/propers/common/
* collect-c*.yml` file (e.g. `collect-c2-1`, not `c2-1` — no suffix
* gets appended), a *template* with a literal `{N}` standing in for
* the saint's own name, substituted via `collectName` below.
* Deliberately more granular than `common` above: the reference
* source's own Commune files split e.g. "Common of a Martyr Bishop"
* into several differently-worded collect templates (its own `C2`,
* `C2-1`, `C2a-1`, `C2b`, `C2b-1` — no clean semantic label recovered
* for *why* each saint gets a particular one, so these ids trace
* straight back to the source's own naming, e.g. `collect-c2-1`,
* rather than inventing a name that might be wrong.
*/
collectCommon?: string;
/** The `{N}` substitution values for `collectCommon`'s template, in
* whatever grammatical case that specific template's own placeholder
* position needs — taken directly from the reference source's own
* per-saint `[Name]` section (Latin/English/Sancti or SanctiM
* `MM-DD.txt`), not re-declined by hand. Arrays, filled in
* left-to-right against the template's own `{N}` occurrences in
* document order — most saints need exactly one name, but several
* Common templates (e.g. `collect-c3`, two co-named Martyr-Bishops)
* have two or more `{N}` placeholders for a group commemorated
* together, one array entry per saint in the group. */
collectName?: { la: string[]; en: string[] };
/**
* Fallback id for Lauds' Benedictus antiphon (`getBenedictusAntiphon`)
* when this saint has no proper `${propers}-antiphon` of their own —
* `data/propers/common/benedictus-antiphon-${id}.yml`. Unlike
* `collectCommon`, no name-substitution template: live-verified every
* Common category's Benedictus antiphon in current use is name-free
* text (the source's own `Commune/C4a.txt`, Doctor, is the one
* exception — has a `{N}`-shaped "beáte N." — but no saint on this
* calendar draws from it yet, so no substitution mechanism was built
* for this field; revisit if that changes). Often, but not always, the
* same category as `minorHoursCommon` below (both were live-verified
* independently per saint, not assumed identical).
*/
benedictusCommon?: string;
/**
* The `{N}` substitution value(s) for the Matins invitatory antiphon's
* own Common template (`hours/matins.ts`'s `resolveMatinsInvitatoryText`,
* via `hours/resolve-common.ts`'s `substituteName` — the same helper
* `getDayCollect` already uses for `collectCommon`/`collectName`, just a
* separate field here since the invitatory's own `{N}` position needs
* its own grammatical case, not necessarily the same one `collectName`
* was declined for). Currently only needed for Common-of-a-Holy-Woman
* (`Commune/C7.txt`'s own `[Invit]`, "Laudémus Deum nostrum * In
* confessióne beátæ N.." — genitive singular, e.g. "Marthæ") — every
* other Common category in use has name-free invitatory text. Absent
* for saints whose Common invitatory antiphon needs no substitution. */
invitatoryName?: { la: string[]; en: string[] };
/** See calendar/types.ts's OctaveConfig doc comment. Absent for the vast
* majority of saints — only ones with a real octave declare it. */
octave?: OctaveConfig;
/** For a `rank: vigil` record only — the real id of the feast this is
* the eve of (e.g. `st-andrew`, not derivable from this record's own id
* by stripping a `vigil-of-` prefix: several vigils' ids don't match
* their feast's id exactly — `vigil-of-the-assumption` vs `assumption`,
* `vigil-of-st-james` vs `st-james-the-greater`). Lets
* calendar/vespers.ts's evening-anticipation merge recognize "today's
* Vigil is being displaced by the very feast it's the eve of" precisely
* (rather than guessing from date adjacency alone) and skip
* commemorating the Vigil redundantly alongside its own feast. */
vigilOf?: string;
/** This feast, when it keeps its own Second Vespers (see
* calendar/vespers.ts's keepsOwnSecondVespers), refuses to commemorate
* whatever wins the following day there — the feast-side mirror of a
* privileged Sunday/major-feria refusing a weak commemoration *back*
* (calendar/commemorations.ts), which vu had no equivalent of at all
* until this field. Modeled as an explicit per-feast flag rather than
* porting the reference engine's fractional-rank privilege matrix
* (crank/rank thresholds, a "Festum Domini" distinction, an outright
* hardcoded pair exception for Most Precious Blood vs. the Sacred
* Heart on the rare late-Easter date they collide) — that matches this
* repo's own "explicit per-feast fact, verified against the live
* engine one date at a time" convention (same as `vigilOf` above)
* rather than a general formula applied blanket and hoped correct.
* Absent (the default) preserves today's status quo: any sanctoral
* successor gets commemorated regardless of rank. Not yet set on any
* saint — add only after checking a specific date against the live
* reference engine, not speculatively for every `duplex-1-classis`
* feast. NOT a fit for the Precious Blood/Sacred Heart case itself
* (that exclusion is specific to one neighbor, not "never commemorate
* anyone" — see this field's own doc comment above) — see TODO.md. */
forbidsSuccessorCommemoration?: true;
}
const sanctoralCalendar = sanctoralCalendarData as { days: Record<string, string[]> };
const saintModules = import.meta.glob<{ default: SaintRecord }>('../data/calendar/saints/*.yml', {
eager: true,
});
const saintsById = new Map<string, SaintRecord>();
for (const mod of Object.values(saintModules)) {
saintsById.set(mod.default.id, mod.default);
}
export interface SanctoralCandidate {
id: string;
name: string;
nameLa?: string;
rank: FeastClass;
}
/** Which saint(s) (if any) are assigned to this date. `isoDate`'s year is ignored — the sanctoral cycle repeats every civil year. */
/** The saint's full record — used when something more than id/name/rank is
* needed, e.g. hours/resolve-common.ts's getDayCollect looking up whether
* a real collect has been authored for a winning saint. */
export function getSaintRecord(id: string): SaintRecord | undefined {
return saintsById.get(id);
}
export function getSanctoralCandidatesFor(isoDate: string): SanctoralCandidate[] {
const monthDay = isoDate.slice(5);
const ids = sanctoralCalendar.days[monthDay] ?? [];
return ids.map((id) => {
const saint = saintsById.get(id);
if (!saint) {
throw new Error(`sanctoral-calendar.yml references unknown saint id '${id}'`);
}
return { id: saint.id, name: saint.name, nameLa: saint.nameLa, rank: saint.rank };
});
}