calendar: model Our Lady's Saturday as a real occurrence outcome
Deploy / deploy (push) Successful in 49s
Deploy / deploy (push) Successful in 49s
On a free Saturday (nothing privileged already claims it, no active octave), the day's own identity is now "Our Lady's Saturday" rather than an anonymous ordinary-feria -- mirrors privileged-feria-minor exactly, per direct instruction: Semiduplex-or-higher still wins outright (Marian Saturday doesn't apply at all); below that (Simplex, and Vigil, both under semiduplex on the FeastClass scale) loses and is commemorated instead. Layered additively after decideOccurrence, same shape as applyOctaves -- never touches decideOccurrence's own rules. Needed a real winner identity (not just the plain temporal feria id) so day-label/antiphon/collect sourcing can recognize it -- added a temporal-feasts record purely for that, and taught getDayLabel to check it for any named temporal winner (a small, free improvement for Christmas/Pentecost's own labels too, previously unhandled). hours: build the duplex-majus+ Lauds psalmody override (per-feast) lauds-psalmody no longer unconditionally uses the plain weekday default: a duplex-majus-or-higher sanctoral winner, or Our Lady's Saturday (unconditional, no rank threshold -- it isn't competing with the weekday default the way a saint is), now substitutes its own proper psalm groups/antiphons/canticle. Per-feast, not per-Common, per direct instruction, even though most duplex-majus+ saints across the year don't have one authored yet and fall back to the plain weekday default until they do. Content for the worked example (St. Lawrence's own day) surfaced a real bug while sourcing it: st-lawrence-antiphon.yml had been read from the Roman/secular rite's own [Ant 1], not Monastic 1617's actual Benedictus antiphon for that day -- corrected from a fresh live query. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
/** Same shape as data/hours/lauds-antiphons.yml's per-weekday entries
|
||||
* (see hours/lauds.ts) — a whole-block substitute for the plain ferial
|
||||
* weekday psalmody, keyed by id: either a saint's own id (gated at
|
||||
* duplex-majus+, see hours/lauds.ts's getPsalmodyOverrideFor) or the
|
||||
* literal 'marian-saturday' (applies unconditionally whenever it wins).
|
||||
* Deliberately per-feast, not per-Common — explicit choice, even though
|
||||
* it means most duplex-majus+ saints across the year don't have one of
|
||||
* these authored yet and fall back to the plain weekday default until
|
||||
* they do (same "mechanism first, content incrementally" pattern as
|
||||
* everywhere else — not a bug, an expected, honest gap). */
|
||||
export interface LaudsPsalmodyOverride {
|
||||
id: string;
|
||||
groups: { psalms: number[]; antiphon: Partial<Record<string, string>> }[];
|
||||
canticle: { id: string; antiphon: Partial<Record<string, string>> };
|
||||
laudate: { antiphon: Partial<Record<string, string>> };
|
||||
}
|
||||
|
||||
const modules = import.meta.glob<{ default: LaudsPsalmodyOverride }>(
|
||||
'../data/hours/lauds-psalmody-overrides/*.yml',
|
||||
{ eager: true },
|
||||
);
|
||||
|
||||
const overrides = new Map<string, LaudsPsalmodyOverride>();
|
||||
for (const mod of Object.values(modules)) {
|
||||
overrides.set(mod.default.id, mod.default);
|
||||
}
|
||||
|
||||
export function getLaudsPsalmodyOverride(id: string): LaudsPsalmodyOverride | undefined {
|
||||
return overrides.get(id);
|
||||
}
|
||||
+45
-9
@@ -1,9 +1,10 @@
|
||||
import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart, ResolvedText } from './types';
|
||||
import type { LiturgicalDay, Weekday } from '../calendar/types';
|
||||
import { resolveDay } from '../calendar';
|
||||
import { resolveDay, isAtLeast } from '../calendar';
|
||||
import { getDayLabel } from '../calendar/day-label';
|
||||
import { getPsalmVerses } from '../psalter';
|
||||
import { getCanticle } from './lauds-canticles';
|
||||
import { getLaudsPsalmodyOverride } from './lauds-psalmody-overrides';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { getMarianAntiphonId, getMarianAntiphonLabel } from './marian-antiphon';
|
||||
import { isDoubleOrHigher } from './antiphon';
|
||||
@@ -18,16 +19,40 @@ interface LaudsGroup {
|
||||
psalms: number[];
|
||||
antiphon: BilingualText;
|
||||
}
|
||||
interface LaudsWeekdayAntiphons {
|
||||
/** Shared by both the plain per-weekday default (data/hours/lauds-
|
||||
* antiphons.yml) and a duplex-majus+/Marian-Saturday override (hours/
|
||||
* lauds-psalmody-overrides.ts) — resolvePsalmody doesn't care which one
|
||||
* produced it. */
|
||||
interface PsalmodyBlock {
|
||||
groups: LaudsGroup[];
|
||||
// `split`: verse count of the canticle's first part, when it's said in
|
||||
// two pieces (own Gloria Patri each) rather than continuously — see
|
||||
// lauds-antiphons.yml's saturday.canticle comment. Absent every other
|
||||
// weekday.
|
||||
// lauds-antiphons.yml's saturday.canticle comment. Only Saturday's
|
||||
// plain default uses this today; no override needs it yet.
|
||||
canticle: { id: string; antiphon: BilingualText; split?: number };
|
||||
laudate: { antiphon: BilingualText };
|
||||
}
|
||||
const laudsAntiphons = laudsAntiphonsData as Record<Weekday, LaudsWeekdayAntiphons>;
|
||||
const laudsAntiphons = laudsAntiphonsData as Record<Weekday, PsalmodyBlock>;
|
||||
|
||||
/**
|
||||
* A duplex-majus+ saint's own proper psalmody (per-feast, not per-Common —
|
||||
* explicit choice), or Marian Saturday's (unconditional whenever it wins,
|
||||
* no rank threshold — it isn't "a saint" competing with the weekday
|
||||
* default the way one is). Falls back to `undefined` — the plain weekday
|
||||
* default — for a duplex-majus+ saint with no override authored yet, same
|
||||
* honest incremental-content convention as everywhere else in this
|
||||
* codebase; it's not gated behind whether content exists, just behind
|
||||
* whether it's *eligible* to override at all.
|
||||
*/
|
||||
function getPsalmodyOverrideFor(day: LiturgicalDay): PsalmodyBlock | undefined {
|
||||
if (day.winner.kind === 'temporal' && day.winner.id === 'marian-saturday') {
|
||||
return getLaudsPsalmodyOverride('marian-saturday');
|
||||
}
|
||||
if (day.winner.kind === 'sanctoral' && isAtLeast(day.winner.rank, 'duplex-majus')) {
|
||||
return getLaudsPsalmodyOverride(day.winner.id);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Mon-Fri share one capitulum verbatim (confirmed against the live engine
|
||||
// — see lauds-capitulum-ferial.yml); Sunday and Saturday each have their
|
||||
@@ -92,9 +117,10 @@ function canticleText(canticleId: string, slice?: [number, number]): ResolvedTex
|
||||
}
|
||||
|
||||
/** Ps 66 through the Laudate psalms — see hours/types.ts's 'lauds-psalmody'
|
||||
* doc comment for the scope and the known Commune-override gap. */
|
||||
* doc comment for the scope, and getPsalmodyOverrideFor above for when the
|
||||
* plain weekday default below gets substituted. */
|
||||
function resolvePsalmody(day: LiturgicalDay): ResolvedPart[] {
|
||||
const wd = laudsAntiphons[day.weekday];
|
||||
const wd = getPsalmodyOverrideFor(day) ?? laudsAntiphons[day.weekday];
|
||||
const opening = (antiphon: BilingualText) => {
|
||||
const { incipit, full } = splitNamedAntiphon(antiphon);
|
||||
return isDoubleOrHigher(day.winner) ? full : incipit;
|
||||
@@ -177,15 +203,25 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
||||
return [{ kind: 'preces', text: resolveCommon(part.textRef.id), label: part.label }];
|
||||
case 'day-collects':
|
||||
return getDayCollects(day).map((text) => ({ kind: 'prayer' as const, text }));
|
||||
case 'suffrages':
|
||||
case 'suffrages': {
|
||||
if (part.omitOnDouble && isDoubleOrHigher(day.winner)) {
|
||||
return [];
|
||||
}
|
||||
return SUFFRAGES.map((id) => ({
|
||||
// Confirmed live: Our Lady's Saturday drops both "Of the Holy Cross"
|
||||
// and "Of the Blessed Virgin Mary" (the latter redundant with the
|
||||
// day's own theme; the former for reasons the reference engine
|
||||
// doesn't explain and this doesn't try to re-derive), keeping only
|
||||
// the Apostles and Peace suffrages.
|
||||
const ids =
|
||||
day.winner.kind === 'temporal' && day.winner.id === 'marian-saturday'
|
||||
? SUFFRAGES.filter((id) => id !== 'lauds-suffrage-cross' && id !== 'lauds-suffrage-bvm')
|
||||
: SUFFRAGES;
|
||||
return ids.map((id) => ({
|
||||
kind: 'preces' as const,
|
||||
text: resolveCommon(id),
|
||||
label: SUFFRAGE_LABELS[id],
|
||||
}));
|
||||
}
|
||||
case 'versicle':
|
||||
case 'chapter':
|
||||
case 'responsory':
|
||||
|
||||
+10
-7
@@ -89,13 +89,16 @@ export type HourPart =
|
||||
// number of antiphoned psalm groups, then a weekday-variable OT canticle,
|
||||
// then the Laudate psalms under one more shared antiphon) doesn't fit any
|
||||
// existing part kind, and hardcoding psalm numbers in lauds.yml can't
|
||||
// work since they differ by weekday. Known, deliberate gap: does NOT
|
||||
// model the Commune/feast override of this whole block (real practice
|
||||
// substitutes an entirely different set of psalms+antiphons+canticle on
|
||||
// a Semiduplex-or-higher day) — every day currently gets the plain
|
||||
// ferial weekday distribution regardless of who wins. Fixing that is a
|
||||
// separate, larger propers-authoring task (a full Common-of-Saints psalm
|
||||
// set per Common), not started yet.
|
||||
// work since they differ by weekday. A duplex-majus-or-higher sanctoral
|
||||
// winner, or Marian Saturday, substitutes its own proper psalmody
|
||||
// instead — see hours/lauds-psalmody-overrides.ts's
|
||||
// getPsalmodyOverrideFor, gated by direct instruction at duplex-majus+
|
||||
// rather than every rank real historical practice would (most saints
|
||||
// were added in ways that override the daily psalms — not wanted here).
|
||||
// Authored per-feast, not per-Common (explicit choice) — known,
|
||||
// deliberate gap: most duplex-majus+ saints across the year don't have
|
||||
// one of these authored yet and fall back to the plain weekday default
|
||||
// until they do (same incremental-content pattern as everywhere else).
|
||||
| { kind: 'lauds-psalmody' }
|
||||
// Lauds only. The weekday-resolved chapter/short-responsory/hymn/closing
|
||||
// versicle bundle that follows the psalmody — see hours/lauds.ts and
|
||||
|
||||
Reference in New Issue
Block a user