Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4bbd78b1fd | |||
| 2f7b421558 |
@@ -1278,6 +1278,88 @@ draft-English antiphon (`tests/hours/resolve-common.test.ts`).
|
||||
simplex", live-verified) both apply before any part runs. See
|
||||
`data/hours/vespers.yml`'s header and `tests/hours/vespers.test.ts`.
|
||||
|
||||
- **Vespers cross-day commemoration now labeled, and feast-side privilege
|
||||
now has a mechanism (2026-08-26)**: a cross-day commemoration
|
||||
(`Commemoration`'s `vespersNote: 'today' | 'tomorrow'`, set only by
|
||||
`calendar/vespers.ts`'s two `commemorationOfDisplaced*` functions) now
|
||||
renders as "(of today)"/"(of tomorrow)" in English, "de præcedenti"/"de
|
||||
sequenti" in Latin (`calendar/day-label.ts`) — the real Divino Afflatu
|
||||
phrasing, per `horascommon.pl`. Also added `forbidsSuccessorCommemoration`
|
||||
to both `SaintRecord` (`calendar/feasts.ts`) and `TemporalFeastRecord`
|
||||
(`calendar/temporal-feasts.ts`) — the feast-side mirror of the day-side
|
||||
privilege `calendar/commemorations.ts` already modeled (a privileged
|
||||
Sunday/major-feria can refuse a weak commemoration *back*; nothing
|
||||
previously let a high-ranked feast refuse to commemorate *forward* at
|
||||
its own kept Second Vespers). Deliberately a flag, not a ported
|
||||
rank-threshold formula (the reference engine's own version is a messy
|
||||
fractional-rank matrix) — set only after checking a specific date
|
||||
against the live engine, same convention as `vigilOf`.
|
||||
- The concrete case that motivated this: **Most Precious Blood (July 1,
|
||||
Duplex I classis) vs. the Feast of the Sacred Heart** (Easter + 68
|
||||
days), which only ever fall adjacent in a very-late-Easter year
|
||||
(Easter in late April). Read the Perl variables carefully (initial
|
||||
pass here got this backwards): it's **Sacred Heart's own kept Second
|
||||
Vespers that refuses to commemorate Precious Blood** the next
|
||||
evening ("Vespera de præcedenti; nihil de sequenti", `$winner` =
|
||||
Sacred Heart/`Pent02-5` = *today* at that point in the branch,
|
||||
`$cwinner` = Precious Blood/`07-01` = *tomorrow*), not the reverse.
|
||||
The other direction — Precious Blood's own Vespers commemorating a
|
||||
Sacred Heart that happens to fall the day *after* it, the other rare
|
||||
late-Easter arrangement — has no matching carve-out in the reference
|
||||
engine at all, so `forbidsSuccessorCommemoration` is deliberately set
|
||||
only on `sacred-heart.yml`, not `most-precious-blood.yml`.
|
||||
- Still open, separately: the reverse gap remains in daytime
|
||||
`decideOccurrence` (`calendar/commemorations.ts`) — a sanctoral
|
||||
winner there unconditionally gets its displaced Sunday/feria
|
||||
commemorated back, regardless of the winner's own rank. Not addressed
|
||||
here; this pass only covered the Vespers cross-day direction.
|
||||
|
||||
- **Ascension, Corpus Christi, and the Feast of the Sacred Heart now
|
||||
modeled as real temporal feasts (2026-08-26)**: none of the three had a
|
||||
`TemporalFeastRecord` at all — only referenced as bare date offsets in
|
||||
`easter-offsets.yml`/`temporal-categories.yml`/
|
||||
`isMajorFixedFeastOfTheLord`. Corpus Christi/Sacred Heart's own days
|
||||
were already blocked from any sanctoral winner (`privileged-feria-
|
||||
major`), but with no record, `resolveTemporalId` fell back to whatever
|
||||
Sunday collect governs that week (`post-pentecost-01` for both), so the
|
||||
day displayed as generic Trinity-week feria content, never "Corpus
|
||||
Christi"/"Sacred Heart" by name. Ascension (Easter+39) was worse:
|
||||
`ascensiontide`'s own `temporal-categories.yml` entry is plain
|
||||
`ordinary-feria` for every day in the season including Ascension
|
||||
Thursday itself, so nothing stopped an ordinary sanctoral candidate
|
||||
from winning that day outright had one ever been scheduled there. All
|
||||
three now have `data/calendar/temporal-feasts/{ascension,corpus-
|
||||
christi,sacred-heart}.yml` (`rank: duplex-1-classis`, an `easter`
|
||||
anchor, `octave: {enabled: true, wins: duplex}`) — the pre-1955
|
||||
calendar specifically (each carries a real octave; the reference
|
||||
engine's own "(rubrica 196)"-tagged variant strips it, later-reform
|
||||
text this project doesn't follow, per CLAUDE.md). Sacred Heart's own
|
||||
record also carries `forbidsSuccessorCommemoration` (see the previous
|
||||
entry above) — the live-sourced Precious Blood exclusion this whole
|
||||
pass started from.
|
||||
- `octave.wins: duplex` is matched to `pentecost-sunday.yml`'s own
|
||||
verified threshold as the closest analogue for all three, and was
|
||||
kept **despite a known, direct counter-example already in this
|
||||
codebase**: `tests/hours/vespers.test.ts`'s St. Ubald case was
|
||||
previously live-verified against Divinum Officium (Monastic
|
||||
Tridentinum 1617) to have him — Semiduplex — win First Vespers
|
||||
*within* Ascension's own octave window, which a strict `duplex`
|
||||
threshold contradicts. Real octaves come in three privilege
|
||||
"orders"; Ascension/Sacred Heart are the weakest (Order III),
|
||||
historically permissive enough for an ordinary Semiduplex saint to
|
||||
still occur inside them, unlike Pentecost's own (Order I) octave
|
||||
this threshold is borrowed from. Kept at `duplex` anyway per direct
|
||||
instruction — the Ubald test (and St. Margaret of Scotland's
|
||||
equivalent First Vespers case, inside Corpus Christi's own window)
|
||||
were updated to assert the new behavior instead of the old
|
||||
live-verified one. Revisit if this turns out to misfire elsewhere;
|
||||
see `ascension.yml`'s own comment for the full note.
|
||||
- No propers content (collects/antiphons/hymns/lessons) authored yet
|
||||
for any of the three — this pass is calendar/occurrence mechanics
|
||||
only, same "content vs. mechanism" split as everywhere else in this
|
||||
file; an hour resolving one of these ids will correctly show
|
||||
`status: 'missing'` text until that's authored.
|
||||
|
||||
### Matins ordo — mechanism built, small content slice (2026-08)
|
||||
|
||||
`src/hours/matins.ts` was the last stub hour. Built as a genuinely different
|
||||
|
||||
@@ -376,6 +376,19 @@ function octaveCommemorationLabel(octave: ActiveOctave): Bi {
|
||||
return octaveCoreName(octave);
|
||||
}
|
||||
|
||||
/** Real Divino Afflatu phrasing for a commemoration that only exists
|
||||
* because Vespers/Compline crossed a day boundary — "commemoratio de
|
||||
* præcedenti" (today, riding along on tomorrow's anticipated First
|
||||
* Vespers) or "commemoratio de sequenti" (tomorrow, riding along on
|
||||
* today's kept Second Vespers), per horascommon.pl. See
|
||||
* `Commemoration`'s `vespersNote` field (calendar/types.ts) and
|
||||
* calendar/vespers.ts's two `commemorationOfDisplaced*` functions, the
|
||||
* only place that sets it. */
|
||||
const CROSS_DAY_VESPERS_LABELS: Record<'today' | 'tomorrow', Bi> = {
|
||||
today: bi('of today', 'de præcedenti'),
|
||||
tomorrow: bi('of tomorrow', 'de sequenti'),
|
||||
};
|
||||
|
||||
/** Every commemoration on `day`, resolved to display strings and grouped
|
||||
* by kind — the single place that decides both "is this commemoration
|
||||
* eligible to show at all here" and "how is it formatted", so every
|
||||
@@ -425,7 +438,12 @@ function collectCommemorations(
|
||||
const octaves: Bi[] = [];
|
||||
for (const c of day.commemorations) {
|
||||
if (c.kind === 'sanctoral') {
|
||||
sanctoral.push(biFallback(c.name));
|
||||
const name = biFallback(c.name);
|
||||
sanctoral.push(
|
||||
c.vespersNote
|
||||
? bi(`${name.en} (${CROSS_DAY_VESPERS_LABELS[c.vespersNote].en})`, `${name.la} (${CROSS_DAY_VESPERS_LABELS[c.vespersNote].la})`)
|
||||
: name,
|
||||
);
|
||||
} else if (c.kind === 'temporal') {
|
||||
if (c.id === temporalId) {
|
||||
temporal.push(anchorDayName(day, false) ?? temporalLabel(day));
|
||||
|
||||
@@ -88,6 +88,27 @@ export interface SaintRecord {
|
||||
* (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[]> };
|
||||
|
||||
@@ -37,6 +37,22 @@ export interface TemporalFeastRecord {
|
||||
* every other movable feast instead loses to an equal-or-higher-ranked
|
||||
* sanctoral winner and is commemorated alongside it instead. */
|
||||
unconditional?: boolean;
|
||||
/** The temporal-feast sibling of calendar/feasts.ts's SaintRecord field
|
||||
* of the same name -- see that field's own doc comment for the full
|
||||
* rationale (a feast-side mirror of the day-side privilege
|
||||
* calendar/commemorations.ts already models, deliberately a flag
|
||||
* rather than a ported rank-threshold formula). Set on sacred-heart.yml
|
||||
* specifically -- live source (horascommon.pl, github #4586): on the
|
||||
* rare late-Easter date the movable Feast of the Sacred Heart falls the
|
||||
* day immediately before the fixed Most Precious Blood (July 1), Sacred
|
||||
* Heart's own kept Second Vespers explicitly does *not* commemorate
|
||||
* Precious Blood the next evening ("Vespera de præcedenti; nihil de
|
||||
* sequenti") -- the reverse direction (Precious Blood's own Vespers
|
||||
* commemorating a Sacred Heart that falls *after* it, the other rare
|
||||
* late-Easter arrangement) has no such carve-out in the reference
|
||||
* engine, so this flag is intentionally one-directional, not mirrored
|
||||
* onto most-precious-blood.yml. */
|
||||
forbidsSuccessorCommemoration?: true;
|
||||
}
|
||||
|
||||
const temporalFeastModules = import.meta.glob<{ default: TemporalFeastRecord }>(
|
||||
@@ -69,7 +85,12 @@ export function movableTemporalFeasts(): TemporalFeastRecord[] {
|
||||
const FIXED_DATE_STARTS: [string, string][] = [['12-25', 'christmas-day']];
|
||||
|
||||
/** Easter-offset starts (offset -> temporal feast id). */
|
||||
const EASTER_OFFSET_STARTS: [number, string][] = [[49, 'pentecost-sunday']];
|
||||
const EASTER_OFFSET_STARTS: [number, string][] = [
|
||||
[39, 'ascension'],
|
||||
[49, 'pentecost-sunday'],
|
||||
[60, 'corpus-christi'],
|
||||
[68, 'sacred-heart'],
|
||||
];
|
||||
|
||||
/** Which temporal feast(s), if any, have their own (day-1) octave start on this date. */
|
||||
export function temporalFeastIdsStartingOn(isoDate: string): string[] {
|
||||
|
||||
+12
-1
@@ -168,7 +168,18 @@ export type DayWinner =
|
||||
*/
|
||||
export type Commemoration =
|
||||
| { kind: 'temporal'; id: string }
|
||||
| ({ kind: 'sanctoral' } & SanctoralIdentity)
|
||||
| ({
|
||||
kind: 'sanctoral';
|
||||
// Set by calendar/vespers.ts's resolveEveningDay when this
|
||||
// commemoration exists only because Vespers/Compline crossed a day
|
||||
// boundary (First Vespers anticipation, or a kept Second Vespers
|
||||
// still noting the displaced neighbor) — 'today' for the real
|
||||
// Divino Afflatu "commemoratio de præcedenti" case, 'tomorrow' for
|
||||
// "commemoratio de sequenti". Left unset for an ordinary same-day
|
||||
// commemoration (a real collision, an octave day, etc.), which
|
||||
// carries no such qualifier.
|
||||
vespersNote?: 'today' | 'tomorrow';
|
||||
} & SanctoralIdentity)
|
||||
| { kind: 'octave'; id: string; name: string };
|
||||
|
||||
export interface LiturgicalDay {
|
||||
|
||||
+21
-4
@@ -14,6 +14,7 @@ import { addDays } from './date-math';
|
||||
import { easterOffsetOf } from './temporal';
|
||||
import { isAtLeast } from './commemorations';
|
||||
import { getSaintRecord } from './feasts';
|
||||
import { getTemporalFeastRecord } from './temporal-feasts';
|
||||
|
||||
/**
|
||||
* Fixed/movable "feasts of the Lord" load-bearing elsewhere in this app
|
||||
@@ -84,7 +85,7 @@ function commemorationOfDisplacedPredecessor(loser: DayWinner): Commemoration |
|
||||
if (loser.kind !== 'sanctoral' || loser.rank === 'simplex') {
|
||||
return undefined;
|
||||
}
|
||||
return { kind: 'sanctoral', id: loser.id, name: loser.name, rank: loser.rank };
|
||||
return { kind: 'sanctoral', id: loser.id, name: loser.name, rank: loser.rank, vespersNote: 'today' };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,12 +99,28 @@ function commemorationOfDisplacedPredecessor(loser: DayWinner): Commemoration |
|
||||
* non-Simplex loser: Jul 25 -> 26 (St. James, Duplex II. classis, kept
|
||||
* second Vespers regardless; St. Anne, Duplex, loses the evening but is
|
||||
* still commemorated).
|
||||
*
|
||||
* `winner` (today's own, the one actually keeping this evening) can
|
||||
* refuse the commemoration outright via its own record's
|
||||
* `forbidsSuccessorCommemoration` — the feast-side mirror of the
|
||||
* day-side privilege calendar/commemorations.ts already models (see that
|
||||
* field's own doc comment, on both SaintRecord and TemporalFeastRecord,
|
||||
* for why this is a flag rather than a rank threshold). Checked on
|
||||
* whichever kind of record `winner` actually is — set today only on
|
||||
* `sacred-heart` (a `temporal` winner), the live-verified case that
|
||||
* motivated this field; every sanctoral saint is still unset, a no-op
|
||||
* until a specific date is verified and flagged.
|
||||
*/
|
||||
function commemorationOfDisplacedSuccessor(loser: DayWinner): Commemoration | undefined {
|
||||
function commemorationOfDisplacedSuccessor(winner: DayWinner, loser: DayWinner): Commemoration | undefined {
|
||||
if (loser.kind !== 'sanctoral') {
|
||||
return undefined;
|
||||
}
|
||||
return { kind: 'sanctoral', id: loser.id, name: loser.name, rank: loser.rank };
|
||||
const forbids =
|
||||
winner.kind === 'sanctoral' ? getSaintRecord(winner.id)?.forbidsSuccessorCommemoration : getTemporalFeastRecord(winner.id)?.forbidsSuccessorCommemoration;
|
||||
if (forbids) {
|
||||
return undefined;
|
||||
}
|
||||
return { kind: 'sanctoral', id: loser.id, name: loser.name, rank: loser.rank, vespersNote: 'tomorrow' };
|
||||
}
|
||||
|
||||
function tagVespersFrom(tomorrow: LiturgicalDay): LiturgicalDay {
|
||||
@@ -196,7 +213,7 @@ export function resolveEveningDay(isoDate: string): LiturgicalDay {
|
||||
// long as it's a real sanctoral winner — see
|
||||
// commemorationOfDisplacedSuccessor's own doc comment for why this
|
||||
// direction doesn't exclude Simplex the way the reverse one does.
|
||||
const commemoration = commemorationOfDisplacedSuccessor(tomorrow.winner);
|
||||
const commemoration = commemorationOfDisplacedSuccessor(today.winner, tomorrow.winner);
|
||||
return commemoration ? { ...today, commemorations: [...today.commemorations, commemoration] } : today;
|
||||
}
|
||||
if (!hasFirstVespers(tomorrow)) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Read from the reference engine: web/www/horas/Latin/Tempora/Pasc5-4.txt,
|
||||
# untagged/default [Rank] block — "Duplex I classis cum Octava Privilegiata
|
||||
# III classis;;7" (the "(rubrica 196)" variant strips the octave entirely,
|
||||
# 1955/1960-reform text this project deliberately doesn't follow — see
|
||||
# CLAUDE.md's pre-1955 calendar/rubrics note). Previously not modeled as
|
||||
# its own record at all: `resolveSeason` already carves Easter+39 into its
|
||||
# own `ascensiontide` season, but nothing gave that Thursday itself a real
|
||||
# identity — `temporal-categories.yml`'s `ascensiontide` entry maps every
|
||||
# feria in the season (including Ascension Thursday itself) to plain
|
||||
# `ordinary-feria`, so before this record existed the day would have shown
|
||||
# generic "Thursday in Ascensiontide" content, with no recognition it's
|
||||
# the feast at all. Duplex I classis outranks any real sanctoral
|
||||
# candidate regardless, so this record's `rank` alone is enough to make
|
||||
# `calendar/movable-feasts.ts`'s `applyMovableFeasts` win the day outright
|
||||
# — the `ordinary-feria` category was never actually load-bearing here.
|
||||
id: ascension
|
||||
name: "The Ascension of Our Lord"
|
||||
rank: duplex-1-classis
|
||||
anchor:
|
||||
kind: easter
|
||||
offset: 39
|
||||
octave:
|
||||
enabled: true
|
||||
# Deliberately `duplex`, matching pentecost-sunday.yml's own verified
|
||||
# threshold as the closest analogue — despite a real, pre-existing
|
||||
# counter-example already in this codebase:
|
||||
# tests/hours/vespers.test.ts's St. Ubald test ("Live-verified against
|
||||
# Divinum Officium (Monastic Tridentinum 1617) votive=C4") has him
|
||||
# (Semiduplex) winning First Vespers on May 15, *within* this octave's
|
||||
# own window, which a strict `duplex` threshold contradicts — real
|
||||
# octaves come in three privilege "orders", and Ascension/Sacred Heart
|
||||
# are the weakest (Order III), historically permissive enough that an
|
||||
# ordinary Semiduplex saint can still occur inside them, unlike
|
||||
# Pentecost's own (Order I) octave this threshold is borrowed from.
|
||||
# Kept at `duplex` anyway per direct instruction (2026-08-26) despite
|
||||
# this known conflict — the Ubald test itself was updated to match, see
|
||||
# its own comment. Revisit if this turns out to misfire elsewhere.
|
||||
wins: duplex
|
||||
@@ -0,0 +1,27 @@
|
||||
# Read from the reference engine: web/www/horas/Latin/Tempora/Pent01-4.txt,
|
||||
# untagged/default [Rank] block — "Duplex I classis cum Octava privilegiata
|
||||
# II ordinis;;7" (the "(rubrica 1960)" variant strips the octave — 1960-
|
||||
# reform text this project deliberately doesn't follow, see CLAUDE.md's
|
||||
# pre-1955 calendar/rubrics note). `temporal-categories.yml` already
|
||||
# forces this single day to `privileged-feria-major` (no sanctoral
|
||||
# candidate can win it), so this record doesn't change *occurrence* on
|
||||
# the feast day itself — what it fixes is identity: without it,
|
||||
# `calendar/temporal-id.ts`'s `resolveTemporalId` falls back to
|
||||
# `post-pentecost-01` (Trinity Sunday's own governing collect) for this
|
||||
# date, so the day would display as generic Trinity-week feria content,
|
||||
# not "Corpus Christi" by name. `rank` here is enough for
|
||||
# `calendar/movable-feasts.ts`'s `applyMovableFeasts` to make this id the
|
||||
# day's real winner.
|
||||
id: corpus-christi
|
||||
name: "The Most Holy Body of Christ"
|
||||
rank: duplex-1-classis
|
||||
anchor:
|
||||
kind: easter
|
||||
offset: 60
|
||||
octave:
|
||||
enabled: true
|
||||
# See ascension.yml's own note (same `duplex` choice, same known
|
||||
# conflict with a live-verified counter-example elsewhere in this
|
||||
# codebase — St. Margaret of Scotland's own First Vespers test, updated
|
||||
# to match) — kept at `duplex` per direct instruction (2026-08-26).
|
||||
wins: duplex
|
||||
@@ -0,0 +1,23 @@
|
||||
# Read from the reference engine: web/www/horas/Latin/Tempora/Pent02-5.txt,
|
||||
# untagged/default [Rank] block — "Duplex I classis cum Octava
|
||||
# privilegiata III ordinis;;6.51" (the "(rubrica 196)" variant strips the
|
||||
# octave — 1955/1960-reform text this project deliberately doesn't
|
||||
# follow, see CLAUDE.md's pre-1955 calendar/rubrics note).
|
||||
# `temporal-categories.yml` already forces this single day to
|
||||
# `privileged-feria-major`; this record's job is identity (a real name
|
||||
# and rank instead of falling back to whatever Sunday governs that week's
|
||||
# collect) and giving `forbidsSuccessorCommemoration` (see
|
||||
# temporal-feasts.ts's own doc comment) a record to live on.
|
||||
id: sacred-heart
|
||||
name: "The Most Sacred Heart of Our Lord Jesus Christ"
|
||||
rank: duplex-1-classis
|
||||
anchor:
|
||||
kind: easter
|
||||
offset: 68
|
||||
octave:
|
||||
enabled: true
|
||||
# See ascension.yml's own note (same `duplex` choice, same known
|
||||
# conflict with a live-verified counter-example, kept per direct
|
||||
# instruction 2026-08-26).
|
||||
wins: duplex
|
||||
forbidsSuccessorCommemoration: true
|
||||
@@ -75,10 +75,14 @@ describe('getDayLabel — ordinal temporal label', () => {
|
||||
|
||||
it('falls back to weekday + season name for seasons with no ordinal convention modeled', () => {
|
||||
// 2026-05-15 is now St. John Baptist de la Salle's own fixed day
|
||||
// (Duplex, added per the full-year sanctoral import) -- moved to
|
||||
// May 21, still within Ascensiontide 2026 (May 14-23) and still
|
||||
// free of any sanctoral entry.
|
||||
expect(getDayLabel(resolveDay('2026-05-21')).en).toContain('in Ascensiontide');
|
||||
// (Duplex, added per the full-year sanctoral import); May 21 is now
|
||||
// Ascension's own octave closing day (Ascension now modeled as a
|
||||
// real TemporalFeastRecord with an octave, Easter+39 through +46 —
|
||||
// see TODO.md's 2026-08-26 entry), and May 22/23 both carry a fixed
|
||||
// saint of their own -- 2028-06-03 (Easter+48, the day before
|
||||
// Pentecost) is the nearest genuinely free Ascensiontide day: past
|
||||
// Ascension's own octave, no fixed saint, still short of Pentecost.
|
||||
expect(getDayLabel(resolveDay('2028-06-03')).en).toContain('in Ascensiontide');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -339,12 +343,20 @@ describe('getDayLabel — named temporal feast rank', () => {
|
||||
});
|
||||
|
||||
describe('getDayLabel — feast name combination', () => {
|
||||
// 2026-06-15 (the "2nd week after Trinity" Monday, Easter+71) used to
|
||||
// be free of any octave; now it's day 4 of Sacred Heart's own octave
|
||||
// (Easter+68 through +75, added 2026-08-26 -- see TODO.md), and that
|
||||
// offset range is fixed relative to Easter every year, not just this
|
||||
// one, so no year-swap avoids it for "2nd week after Trinity"
|
||||
// specifically. Moved to the "3rd week after Trinity" Monday
|
||||
// (Easter+78) instead, clear of Ascension's (+39..+46) and the
|
||||
// Corpus Christi/Sacred Heart combined (+60..+75) octave ranges.
|
||||
const base: LiturgicalDay = {
|
||||
date: '2026-06-15',
|
||||
date: '2026-06-22',
|
||||
weekday: 'monday',
|
||||
season: 'trinitytide',
|
||||
temporalCategory: 'ordinary-feria',
|
||||
winner: { kind: 'temporal', id: 'post-pentecost-02' },
|
||||
winner: { kind: 'temporal', id: 'post-pentecost-03' },
|
||||
commemorations: [],
|
||||
};
|
||||
|
||||
@@ -361,10 +373,10 @@ describe('getDayLabel — feast name combination', () => {
|
||||
...base,
|
||||
commemorations: [{ kind: 'sanctoral', id: 'x', name: 'St. Ereden', rank: 'duplex-2-classis' }],
|
||||
};
|
||||
expect(getDayLabel(day).en).toBe('St. Ereden — Monday in the 2nd week after Trinity (Feria)');
|
||||
expect(getDayLabel(day).en).toBe('St. Ereden — Monday in the 3rd week after Trinity (Feria)');
|
||||
});
|
||||
|
||||
it('shows just the temporal label when nothing is commemorated at all', () => {
|
||||
expect(getDayLabel(base).en).toBe('Monday in the 2nd week after Trinity (Feria)');
|
||||
expect(getDayLabel(base).en).toBe('Monday in the 3rd week after Trinity (Feria)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,8 +10,12 @@ describe('resolveEveningDay', () => {
|
||||
});
|
||||
|
||||
it('does not anticipate between two plain ferias', () => {
|
||||
const day = resolveEveningDay('2026-06-16'); // Tuesday, ordinary trinitytide
|
||||
expect(day.date).toBe('2026-06-16');
|
||||
// 2033-09-06: Tuesday, ordinary trinitytide, genuinely free of any
|
||||
// saint or active octave on both this date and the next (2026-06-16,
|
||||
// used here previously, fell inside Sacred Heart's own octave once
|
||||
// that was modeled 2026-08-26 -- see TODO.md).
|
||||
const day = resolveEveningDay('2033-09-06');
|
||||
expect(day.date).toBe('2033-09-06');
|
||||
});
|
||||
|
||||
it('anticipates Easter from Holy Saturday', () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { resolveOrdo } from '../../src/hours';
|
||||
// why these particular dates.
|
||||
const FERIAL_SUNDAY = '2026-06-21';
|
||||
const FERIAL_MONDAY = '2026-06-08'; // not 08-31 -- St. Raymond Nonnatus's own day (common-of-a-confessor)
|
||||
const FERIAL_TUESDAY = '2026-06-16';
|
||||
const FERIAL_TUESDAY = '2033-09-06'; // not 2026-06-16 -- that fell inside Sacred Heart's own octave once modeled 2026-08-26, see TODO.md
|
||||
const LAWRENCE_OCTAVE_DAY = '2026-08-11'; // Semiduplex -- exercises isDoubleOrHigher's false branch differently than a plain feria only in rank, not in psalms (Commune override not modeled, see below)
|
||||
|
||||
describe('resolveOrdo("lauds", ...)', () => {
|
||||
@@ -170,11 +170,17 @@ describe('resolveOrdo("lauds", ...)', () => {
|
||||
expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Martyr Dei, qui únicum');
|
||||
});
|
||||
|
||||
it("uses Common-of-a-Confessor-Bishop's real office bundle on St. Ubald's own day, not the ferial default", () => {
|
||||
// St. Ubald (May 16, Semiduplex, common-of-a-confessor-bishop) has no
|
||||
// proper Lauds content of his own -- see vespers-hymn-common-of-a-
|
||||
// confessor-bishop.yml's header.
|
||||
const ordo = resolveOrdo('lauds', '2026-05-16');
|
||||
it("uses Common-of-a-Confessor-Bishop's real office bundle on St. Robert Bellarmine's own day, not the ferial default", () => {
|
||||
// Was St. Ubald (May 16, Semiduplex, common-of-a-confessor-bishop) --
|
||||
// moved to St. Robert Bellarmine (May 13, Duplex, same Common) once
|
||||
// Ascension's own octave was modeled (2026-08-26, see TODO.md):
|
||||
// Ubald now loses his own day outright to the octave (`octave.wins:
|
||||
// duplex`, and Semiduplex Ubald falls short of it), so his own
|
||||
// Common-of-a-Confessor-Bishop content no longer surfaces there at
|
||||
// all. Bellarmine sits safely before the octave's own window (May
|
||||
// 14-21) and has no proper Lauds content of his own either -- see
|
||||
// vespers-hymn-common-of-a-confessor-bishop.yml's header.
|
||||
const ordo = resolveOrdo('lauds', '2026-05-13');
|
||||
const chapter = ordo.parts.find((p) => p.kind === 'chapter');
|
||||
expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Sir 44:16-17');
|
||||
const hymn = ordo.parts.find((p) => p.kind === 'hymn');
|
||||
|
||||
+41
-19
@@ -129,10 +129,11 @@ describe('resolveOrdo("vespers", ...)', () => {
|
||||
});
|
||||
|
||||
it('falls back to the classic weekday-default Magnificat antiphon on a bare ferial', () => {
|
||||
// 2026-06-16 is a Tuesday, plain ordinary trinitytide, no saint, no
|
||||
// 2033-09-06 is a Tuesday, plain ordinary trinitytide, no saint, no
|
||||
// active octave (same date already used by calendar/vespers.test.ts's
|
||||
// "does not anticipate between two plain ferias").
|
||||
const ordo = resolveOrdo('vespers', '2026-06-16');
|
||||
// "does not anticipate between two plain ferias" -- see that test's
|
||||
// own comment for why this moved off 2026-06-16).
|
||||
const ordo = resolveOrdo('vespers', '2033-09-06');
|
||||
const magnificatIndex = ordo.parts.findIndex((p) => p.kind === 'canticle' && p.canticleId === 'magnificat');
|
||||
expect(magnificatIndex).toBeGreaterThanOrEqual(0);
|
||||
const magnificat = ordo.parts[magnificatIndex];
|
||||
@@ -250,15 +251,24 @@ describe('resolveOrdo("vespers", ...)', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("uses Common-of-a-Confessor-Bishop's real office bundle for St. Ubald's First Vespers", () => {
|
||||
// St. Ubald (May 16, Semiduplex, common-of-a-confessor-bishop) claims
|
||||
// First Vespers over May 15's own evening (no stronger rival that
|
||||
// year). Live-verified against Divinum Officium (Monastic Tridentinum
|
||||
// 1617) votive=C4 -- see vespers-hymn-common-of-a-confessor-bishop.yml's
|
||||
// header.
|
||||
it("no longer gives St. Ubald First Vespers now that Ascension's own octave is modeled", () => {
|
||||
// Historically (live-verified against Divinum Officium, Monastic
|
||||
// Tridentinum 1617, votive=C4) St. Ubald (May 16, Semiduplex,
|
||||
// common-of-a-confessor-bishop) claims First Vespers over May 15's
|
||||
// own evening -- a real Order-III-octave permissiveness this app's
|
||||
// own `ascension.yml` (added 2026-08-26) doesn't reproduce: its
|
||||
// `octave.wins: duplex` is deliberately borrowed from Pentecost's own
|
||||
// (Order I) threshold rather than modeled per privilege-order, a
|
||||
// known, accepted conflict with this specific live-verified case
|
||||
// (see ascension.yml's own comment). Semiduplex Ubald now loses to
|
||||
// the octave outright on his own day (May 16), so May 15's evening is
|
||||
// just St. John Baptist de la Salle's (Duplex, wins May 15 itself
|
||||
// outright) own kept Second Vespers -- his own Common-of-a-Confessor
|
||||
// (not Bishop) bundle, not Ubald's Confessor-Bishop one -- with
|
||||
// Ascension's octave merely commemorated alongside him.
|
||||
const ordo = resolveOrdo('vespers', '2026-05-15');
|
||||
const chapter = ordo.parts.find((p) => p.kind === 'chapter');
|
||||
expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Sir 44:16-17');
|
||||
expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Sir 31:8-9');
|
||||
const hymn = ordo.parts.find((p) => p.kind === 'hymn');
|
||||
expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Iste Conféssor Dómini sacrátus');
|
||||
});
|
||||
@@ -322,16 +332,28 @@ describe('resolveOrdo("vespers", ...)', () => {
|
||||
expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Exsúltet cælum láudibus');
|
||||
});
|
||||
|
||||
it("uses common-of-a-widow's real office bundle for St. Margaret of Scotland's First Vespers", () => {
|
||||
it("no longer gives St. Margaret of Scotland First Vespers now that Corpus Christi's own octave is modeled", () => {
|
||||
// St. Margaret of Scotland (Jun 10, Semiduplex, common-of-a-widow --
|
||||
// live-verified 2026-08-26 to be real content identical to Common-
|
||||
// of-a-Holy-Woman's, see lauds-capitulum-common-of-a-holy-woman.yml's
|
||||
// header) claims First Vespers over Jun 9's own evening.
|
||||
// content itself live-verified 2026-08-26 to be real, identical to
|
||||
// Common-of-a-Holy-Woman's, see lauds-capitulum-common-of-a-holy-
|
||||
// woman.yml's header) used to claim First Vespers over Jun 9's own
|
||||
// evening. Now inside Corpus Christi's own octave (Jun 4-11, added
|
||||
// 2026-08-26, `octave.wins: duplex` -- same deliberate, accepted
|
||||
// conflict with a real Order-III-octave permissiveness as
|
||||
// ascension.yml's own St. Ubald case, see corpus-christi.yml's own
|
||||
// comment), Semiduplex Margaret loses her own day (Jun 10) outright
|
||||
// to the octave -- and Jun 9 itself is *also* inside the same
|
||||
// octave, already having lost its own Simplex saint (Ss. Primus and
|
||||
// Felicianus) to it, so neither day has any sanctoral winner left to
|
||||
// anticipate. Jun 9's evening falls all the way through to the plain
|
||||
// ferial Vespers default (Trinity week's own governing chapter/hymn),
|
||||
// with both Primus & Felicianus and Corpus Christi's octave merely
|
||||
// commemorated, not any widow-specific content at all.
|
||||
const ordo = resolveOrdo('vespers', '2026-06-09');
|
||||
const chapter = ordo.parts.find((p) => p.kind === 'chapter');
|
||||
expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('Prov 31:10-11');
|
||||
expect(chapter?.kind === 'chapter' ? chapter.text.citation?.en : undefined).toBe('2 Cor 1:3-4');
|
||||
const hymn = ordo.parts.find((p) => p.kind === 'hymn');
|
||||
expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Fortem viríli péctore');
|
||||
expect(hymn?.kind === 'hymn' ? hymn.text.text.la : undefined).toContain('Tellúris ingens Cónditor');
|
||||
});
|
||||
|
||||
it("uses common-of-the-bvm's real office bundle for Our Lady of Mount Carmel's First Vespers", () => {
|
||||
@@ -409,9 +431,9 @@ describe('resolveOrdo("vespers", ...)', () => {
|
||||
});
|
||||
|
||||
it('includes the Kyrie + Our Father lead-in right after the Magnificat, before the day collect (either form)', () => {
|
||||
// 2026-06-16 is a Tuesday, plain ordinary trinitytide -- same bare
|
||||
// 2033-09-06 is a Tuesday, plain ordinary trinitytide -- same bare
|
||||
// ferial already used above for the Magnificat weekday-default test.
|
||||
const ordo = resolveOrdo('vespers', '2026-06-16');
|
||||
const ordo = resolveOrdo('vespers', '2033-09-06');
|
||||
const litanyIndex = ordo.parts.findIndex((p) => p.kind === 'preces' && p.text.text.la?.includes('Kýrie, eléison'));
|
||||
const magnificatIndex = ordo.parts.findIndex((p) => p.kind === 'canticle' && p.canticleId === 'magnificat');
|
||||
const collectIndex = ordo.parts.findIndex((p) => p.kind === 'prayer');
|
||||
@@ -424,7 +446,7 @@ describe('resolveOrdo("vespers", ...)', () => {
|
||||
// dropped from this app's own text (2026-08) — 'Ego dixi' is the next
|
||||
// versicle after where the psalm used to sit, still unique to the
|
||||
// fuller ferial form vs. the short litany.
|
||||
const ordo = resolveOrdo('vespers', '2026-06-16');
|
||||
const ordo = resolveOrdo('vespers', '2033-09-06');
|
||||
const preces = ordo.parts.find((p) => p.kind === 'preces' && p.text.text.la?.includes('Kýrie, eléison'));
|
||||
expect(preces?.kind === 'preces' ? preces.text.text.la : undefined).toContain('Ego dixi: Dómine, miserére mei');
|
||||
expect(preces?.kind === 'preces' ? preces.text.text.en : undefined).toContain('I said: Lord, be merciful');
|
||||
|
||||
Reference in New Issue
Block a user