Compare commits

..

2 Commits

Author SHA1 Message Date
will 90665429ae Author Easter Octave Wednesday-Saturday's shared Matins antiphons
Deploy / deploy (push) Successful in 1m28s
All 4 weekdays share the exact same 2 antiphons verbatim ("Jesum qui
crucifíxus est quǽritis, allelúja..." / "Veníte et vidéte locum ubi
pósitus erat Dóminus, allelúja, allelúja."), live-verified against
Pofficium.pl (Monastic Tridentinum 1617, prayMatutinum) across
2026-04-08/09/10/11 — despite each day drawing on a different Gospel
reading in the nocturn-readings pool, the antiphon layer doesn't vary
by day.

Layered onto vu's own already-fixed per-weekday ferial psalm numbers
rather than the reference engine's own differently-sized (Rule of
Benedict summer-contracted) psalm set for these days, consistent with
this project's deliberate rejection of that contraction.

This closes "Easter's own octave" fully: calendar precedence
(2026-09-01), Sunday/Monday/Tuesday's shared proper psalmody, and now
Wednesday-Saturday's antiphons (both 2026-09-02).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X26BkFT3ARbtUGXuBmzUmD
2026-09-02 07:51:34 -04:00
will f78222ce9e Let a temporal winner's ferial branch use its own named antiphons
Ferial-side counterpart to the Sunday-branch temporal-override
mechanism added earlier today: hours/matins.ts's ferialAntiphonedNocturn
now checks a new matins-ferial-named-antiphon-overrides.yml store first,
before falling back to the plain per-weekday default. An override's
antiphon list is distributed evenly across the weekday's own
already-fixed psalm-group count rather than tied to specific psalm
numbers, since one entry is meant to be reused across several
different weekdays (each with a different psalm set) instead of
authored per-day. Same optional weekdays field as the Sunday-side
override restricts which days of a multi-day temporalId span actually
share the content.

General-purpose, not scoped to any one feast — content added next.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X26BkFT3ARbtUGXuBmzUmD
2026-09-02 07:51:06 -04:00
4 changed files with 156 additions and 15 deletions
+31 -11
View File
@@ -19,13 +19,13 @@ across every hour and content type in this app.
1. **Mechanism gaps** — a real design/code piece not built yet, blocking any content pass on 1. **Mechanism gaps** — a real design/code piece not built yet, blocking any content pass on
it from starting: it from starting:
- ~~Easter's own octave~~ — DONE (2026-09-01/02): the transfer-cascade piece (calendar - ~~Easter's own octave~~ — DONE (2026-09-01/02): calendar precedence (the transfer-cascade
precedence within Holy Week/Easter Day/the Octave/Low Sunday) and Easter Sunday/Monday/ piece within Holy Week/Easter Day/the Octave/Low Sunday), Easter Sunday/Monday/Tuesday's
Tuesday's own proper Matins (psalms/antiphons/versicles/canticles, sharing one authored own proper Matins, and Wednesday-Saturday's own antiphons are all built, authored, and
entry via a new general-purpose temporal-override mechanism) are both built and
live-verified — see the dated log's "Easter octave transfer cascade" and "Easter's own live-verified — see the dated log's "Easter octave transfer cascade" and "Easter's own
octave" entries. What's left is narrower, moved to the bulk-content section below: octave" entries (two general-purpose mechanisms came out of this: a Sunday-branch and a
Wednesday-Saturday of the Octave's own (different, not yet authored) antiphons. ferial-branch temporal-named-override, both reusable by any future temporal feast wanting
its own proper psalms/lessons, not scoped to Easter specifically).
- Date picker: `day-nav.ts` currently only steps a day at a time (prev/next); there's no - Date picker: `day-nav.ts` currently only steps a day at a time (prev/next); there's no
way to jump to an arbitrary date without walking there one day at a time or editing the way to jump to an arbitrary date without walking there one day at a time or editing the
URL. Needs a calendar-grid picker UI — open, not started. URL. Needs a calendar-grid picker UI — open, not started.
@@ -53,11 +53,6 @@ across every hour and content type in this app.
nocturn-readings sweep" below, including the easter-sunday/pentecost-sunday correction). nocturn-readings sweep" below, including the easter-sunday/pentecost-sunday correction).
Moved out of this "bulk content pending" section since there's no more bulk content left to Moved out of this "bulk content pending" section since there's no more bulk content left to
author here. author here.
- Wednesday-Saturday of the Easter Octave: real Paschal-alleluia antiphons exist in the
reference engine (live-verified 2026-04-08) over a differently-sized psalm set than vu's
own fixed ferial table — 4 days of antiphon-only content, same shape as the existing
Advent/Paschaltide seasonal-overlay pattern, layered onto vu's own psalm numbers rather
than the reference engine's. See the dated log's "Easter's own octave" entry.
- The bulk Vulgate (Latin) + Douay-Rheims (English) scripture import into - The bulk Vulgate (Latin) + Douay-Rheims (English) scripture import into
`src/data/scripture/` — long-term project, explicitly deferred; Matins scripture `src/data/scripture/` — long-term project, explicitly deferred; Matins scripture
citations resolve to `missing` text until it lands. citations resolve to `missing` text until it lands.
@@ -4934,3 +4929,28 @@ seasonal-overlay pattern, layered onto vu's own ferial psalm numbers rather than
reproduce the reference engine's differently-sized set — not started. reproduce the reference engine's differently-sized set — not started.
`npm test` (828 passed), `tsc --noEmit`, and `npm run build` all pass. `npm test` (828 passed), `tsc --noEmit`, and `npm run build` all pass.
### Easter's own octave — Wednesday-Saturday's antiphons done too, fully closing the item (2026-09-02, same day)
Closes the remaining open piece from the entry above. Live-verified: all 4 weekdays
(Wednesday-Saturday) share the exact same 2 antiphons verbatim — not per-day content, despite
each drawing on a different Gospel reading in the nocturn-readings pool ("Jesum qui crucifíxus
est quǽritis, allelúja..." / "Veníte et vidéte locum ubi pósitus erat Dóminus, allelúja,
allelúja.", Pofficium.pl, prayMatutinum, Monastic Tridentinum 1617, 2026-04-08/09/10/11).
New general-purpose mechanism, the ferial-side counterpart to the Sunday-named-override built
earlier the same day: `data/hours/matins-ferial-named-antiphon-overrides.yml` (a temporal id's
own antiphon list, optionally restricted to specific weekdays — same `weekdays` idiom as the
Sunday-side override) + `hours/matins.ts`'s `ferialNamedAntiphonedNocturn`, checked first in
`ferialAntiphonedNocturn` ahead of the plain per-weekday default. The antiphon list is
distributed evenly across the weekday's own already-fixed psalm-group count (not tied to
specific psalm numbers), since one entry is meant to be reused across several different
weekdays rather than authored per-day — psalm numbers themselves are unchanged from the plain
ferial default (vu's own fixed table, not the reference engine's differently-sized Paschaltide-
contracted one).
With this, "Easter's own octave" is fully closed: calendar precedence (2026-09-01), Sunday/
Monday/Tuesday's shared proper psalmody (2026-09-02), and now Wednesday-Saturday's antiphons
(2026-09-02, same day). See memory `vu_easter_octave_proper_matins` for the full writeup.
`npm test` (832 passed), `tsc --noEmit`, and `npm run build` all pass.
@@ -0,0 +1,37 @@
# Ferial (1-nocturn) Matins antiphon override for a specific temporal id
# whose plain per-weekday antiphons (matins-ferial-antiphons.yml) don't
# apply — the ferial-side counterpart to matins-sunday-named-nocturn-
# overrides.yml's `weekdays` field/general mechanism, added for the same
# reason: some temporal feasts have their own real antiphon content that
# should override the plain weekday default without a new part kind or
# psalm-set change.
#
# `antiphons` is an ordered list, distributed as evenly as possible across
# the target weekday's own already-fixed psalm-group count (from
# matins-ferial-antiphons.yml) — NOT tied to specific psalm numbers, since
# one entry here is meant to be reused across several different weekdays
# (each with its own different psalm set) rather than authored per-day.
# `hours/matins.ts`'s `ferialNamedAntiphonedNocturn` does the splitting.
#
# Easter Octave, Wednesday-Saturday (`easter-sunday`, restricted via
# `weekdays` the same way the Sunday-side override restricts itself to
# just Sunday/Monday/Tuesday of the same span): live-verified (Pofficium.pl,
# prayMatutinum, Monastic Tridentinum 1617, 2026-04-08/09/10/11) that all 4
# weekdays share the exact same 2 antiphons verbatim — not per-day content,
# despite each having a different Gospel reading in the nocturn-readings
# pool. The reference engine itself renders these 2 antiphons over its own
# 2-nocturn, differently-sized psalm set (the historical Rule-of-Benedict
# Paschaltide contraction) — not reproduced here, since this app already
# deliberately rejects that contraction (see CLAUDE.md's "No RB summer
# contraction"). Instead, layered onto vu's own already-fixed per-weekday
# 9-psalm ferial pool, split into 2 roughly-even halves by psalm-group
# count (a design choice, not a transcription — the reference's own
# antiphon/psalm-group boundary doesn't map onto vu's differently-sized
# pool at all).
easter-sunday:
weekdays: [wednesday, thursday, friday, saturday]
antiphons:
- la: "Jesum qui crucifíxus est quǽritis, allelúja; non est hic: surréxit enim, sicut dixit vobis, allelúja."
en: "You seek Jesus who was crucified, alleluia; he is not here, for he is risen, as he said, alleluia."
- la: "Veníte et vidéte locum ubi pósitus erat Dóminus, allelúja, allelúja."
en: "Come, see the place where the Lord was laid, alleluia, alleluia."
+53 -2
View File
@@ -99,6 +99,7 @@ import matinsSundayAdventOverridesData from '../data/hours/matins-sunday-advent-
import matinsSundayPaschaltideOverridesData from '../data/hours/matins-sunday-paschaltide-nocturn-overrides.yml'; import matinsSundayPaschaltideOverridesData from '../data/hours/matins-sunday-paschaltide-nocturn-overrides.yml';
import matinsSundayNamedOverridesData from '../data/hours/matins-sunday-named-nocturn-overrides.yml'; import matinsSundayNamedOverridesData from '../data/hours/matins-sunday-named-nocturn-overrides.yml';
import matinsFerialAntiphonsData from '../data/hours/matins-ferial-antiphons.yml'; import matinsFerialAntiphonsData from '../data/hours/matins-ferial-antiphons.yml';
import matinsFerialNamedOverridesData from '../data/hours/matins-ferial-named-antiphon-overrides.yml';
import type { Weekday } from '../calendar/types'; import type { Weekday } from '../calendar/types';
type BilingualText = Partial<Record<string, string>>; type BilingualText = Partial<Record<string, string>>;
@@ -227,6 +228,18 @@ interface FerialNocturn {
type MatinsFerialAntiphons = Record<Exclude<Weekday, 'sunday'>, FerialNocturn>; type MatinsFerialAntiphons = Record<Exclude<Weekday, 'sunday'>, FerialNocturn>;
const ferialAntiphons = matinsFerialAntiphonsData as unknown as MatinsFerialAntiphons; const ferialAntiphons = matinsFerialAntiphonsData as unknown as MatinsFerialAntiphons;
// Ferial-side counterpart to `MatinsSundayNamedOverride` — a temporal id
// whose own real antiphons should replace the plain per-weekday default
// in the 1-nocturn branch, reused as-is across however many weekdays
// `weekdays` names (own psalm numbers per weekday unchanged, only the
// antiphon layer swaps) rather than authored per-weekday — see
// matins-ferial-named-antiphon-overrides.yml's own header.
interface FerialNamedOverride {
weekdays: Weekday[];
antiphons: BilingualText[];
}
const ferialNamedOverrides = matinsFerialNamedOverridesData as unknown as Record<string, FerialNamedOverride>;
const NOCTURN_NUMERAL = ['I', 'II', 'III'] as const; const NOCTURN_NUMERAL = ['I', 'II', 'III'] as const;
/** Combines a versicle's V. and R. lines into one rendered block — every /** Combines a versicle's V. and R. lines into one rendered block — every
@@ -443,7 +456,12 @@ function resolveMatinsHymn(day: LiturgicalDay): ResolvedText {
return resolveCommon('matins-hymn-ferial'); return resolveCommon('matins-hymn-ferial');
} }
/** The plain ferial weekday nocturn's real antiphoned psalmody (see /** The plain ferial weekday nocturn's real antiphoned psalmody — a
* temporal winner with a matching `ferialNamedOverrides` entry (2026-09-02,
* e.g. the Easter Octave's Wed-Sat) takes precedence and swaps in that
* override's own antiphons via `ferialNamedAntiphonedNocturn` instead (see
* data/hours/matins-ferial-named-antiphon-overrides.yml's own header);
* otherwise the plain per-weekday default (see
* data/hours/matins-ferial-antiphons.yml's own header for how each * data/hours/matins-ferial-antiphons.yml's own header for how each
* weekday's groups were reconciled against psalter-distribution.yml's own * weekday's groups were reconciled against psalter-distribution.yml's own
* "editorial redistribution" of the historical per-weekday psalm set). * "editorial redistribution" of the historical per-weekday psalm set).
@@ -454,8 +472,12 @@ function resolveMatinsHymn(day: LiturgicalDay): ResolvedText {
* real single-nocturn ferial office runs straight from the last psalm to * real single-nocturn ferial office runs straight from the last psalm to
* the Capitulum (fixed 2026-09-02, see the data file's own header). */ * the Capitulum (fixed 2026-09-02, see the data file's own header). */
function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] { function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] {
const nocturn = ferialAntiphons[day.weekday as Exclude<Weekday, 'sunday'>];
const winner = resolveOfficeWinner(day); const winner = resolveOfficeWinner(day);
const namedOverride = winner.kind === 'temporal' ? ferialNamedOverrides[winner.id] : undefined;
if (namedOverride && namedOverride.weekdays.includes(day.weekday)) {
return ferialNamedAntiphonedNocturn(day, namedOverride, winner);
}
const nocturn = ferialAntiphons[day.weekday as Exclude<Weekday, 'sunday'>];
const parts: ResolvedPart[] = []; const parts: ResolvedPart[] = [];
for (const group of nocturn.groups) { for (const group of nocturn.groups) {
const antiphonText = verifiedText(group.antiphon); const antiphonText = verifiedText(group.antiphon);
@@ -476,6 +498,35 @@ function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] {
return parts; return parts;
} }
/** A ferial named override's antiphons, distributed as evenly as possible
* across the weekday's own already-fixed psalm-group count (see
* matins-ferial-named-antiphon-overrides.yml's own header) — the psalm
* numbers themselves are unchanged from the plain per-weekday default,
* only the antiphon layer swaps. */
function ferialNamedAntiphonedNocturn(day: LiturgicalDay, override: FerialNamedOverride, winner: DayWinner): ResolvedPart[] {
const nocturn = ferialAntiphons[day.weekday as Exclude<Weekday, 'sunday'>];
const chunkSize = Math.ceil(nocturn.groups.length / override.antiphons.length);
const parts: ResolvedPart[] = [];
override.antiphons.forEach((antiphon, i) => {
const chunk = nocturn.groups.slice(i * chunkSize, (i + 1) * chunkSize);
if (chunk.length === 0) return;
const antiphonText = verifiedText(antiphon);
const opening = openingAntiphon(antiphonText, winner);
const { full } = splitNamedAntiphon(antiphonText);
let isFirst = true;
for (const group of chunk) {
for (const ref of group.psalms) {
const rawVerses = getPsalmVerses(ref.number, ref.verses).map((v): ResolvedVerse => ({ n: v.n, text: v.text, status: v.status }));
const { verses, antiphon: partAntiphon } = applyFlexaMark(rawVerses, isFirst ? opening : undefined);
isFirst = false;
parts.push({ kind: 'psalm', psalmNumber: ref.number, verses, antiphon: partAntiphon });
}
}
parts.push({ kind: 'antiphon', text: full });
});
return parts;
}
/** Fallback for a Semiduplex+ weekday feast with no matins-psalmody-overrides /** Fallback for a Semiduplex+ weekday feast with no matins-psalmody-overrides
* entry authored for its winner yet: the plain ferial weekday table, * entry authored for its winner yet: the plain ferial weekday table,
* chunked into 3 nocturns instead of 1. Bare (no antiphons, no canticles) * chunked into 3 nocturns instead of 1. Bare (no antiphons, no canticles)
+35 -2
View File
@@ -391,12 +391,14 @@ describe('resolveOrdo("matins", ...) Easter Sunday/Monday/Tuesday shared proper
expect(nocturn3?.text.text.la).toBe('V. Gavísi sunt discípuli, allelúja.\nR. Viso Dómino, allelúja.'); expect(nocturn3?.text.text.la).toBe('V. Gavísi sunt discípuli, allelúja.\nR. Viso Dómino, allelúja.');
}); });
it('Wednesday-Saturday of the Octave (same winner.id, excluded by `weekdays`) keep the plain 1-nocturn ferial branch unchanged', () => { it('Wednesday-Saturday of the Octave (same winner.id, excluded by the Sunday-side `weekdays`) stay in the plain 1-nocturn ferial branch, same psalm numbers, no Te Deum', () => {
const ordo = resolveOrdo('matins', '2026-04-08'); // Wednesday of Easter week const ordo = resolveOrdo('matins', '2026-04-08'); // Wednesday of Easter week
const day = resolveDay('2026-04-08'); const day = resolveDay('2026-04-08');
expect(day.winner).toEqual({ kind: 'temporal', id: 'easter-sunday' }); expect(day.winner).toEqual({ kind: 'temporal', id: 'easter-sunday' });
const psalms = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber); const psalms = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
// Plain ferial Wednesday table, same as any other Wednesday. // Plain ferial Wednesday psalm numbers, same as any other Wednesday --
// only the antiphon layer differs (see the ferial-named-override
// describe block below), not the psalmody itself.
expect(psalms.slice(2)).toEqual(getPsalmsFor('matins', 'wednesday').map((r) => r.number)); expect(psalms.slice(2)).toEqual(getPsalmsFor('matins', 'wednesday').map((r) => r.number));
expect(ordo.parts.some((p) => p.kind === 'te-deum')).toBe(false); expect(ordo.parts.some((p) => p.kind === 'te-deum')).toBe(false);
}); });
@@ -408,6 +410,37 @@ describe('resolveOrdo("matins", ...) Easter Sunday/Monday/Tuesday shared proper
}); });
}); });
describe('resolveOrdo("matins", ...) Easter Octave Wednesday-Saturday ferial named antiphon override (2026-09-02 fix)', () => {
// All 4 weekdays share the exact same 2 antiphons verbatim (live-
// verified 2026-04-08/09/10/11) -- own psalm numbers per weekday
// unchanged, split evenly across the weekday's own psalm-group count.
const EASTER_OCTAVE_WEEKDAYS = ['2026-04-08', '2026-04-09', '2026-04-10', '2026-04-11'] as const;
it.each(EASTER_OCTAVE_WEEKDAYS)('%s gets the same 2 real antiphons, own weekday psalm numbers unchanged', (date) => {
const day = resolveDay(date);
expect(day.winner).toEqual({ kind: 'temporal', id: 'easter-sunday' });
const ordo = resolveOrdo('matins', date);
// Ps 3 + 94 (opening/invitatory) plus this weekday's own real ferial table.
const psalms = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
expect(psalms.slice(2)).toEqual(getPsalmsFor('matins', day.weekday as Exclude<typeof day.weekday, 'sunday'>).map((r) => r.number));
// Only the 2 named-override antiphons appear in the nocturn (skip the
// Invitatory's own antiphon, index 0).
const antiphons = ordo.parts.filter((p) => p.kind === 'antiphon') as { text: { text: Record<string, string> } }[];
const [, first, second] = antiphons;
expect(first?.text.text.la).toBe(
'Ant. Jesum qui crucifíxus est quǽritis, allelúja; non est hic: surréxit enim, sicut dixit vobis, allelúja.',
);
expect(second?.text.text.la).toBe('Ant. Veníte et vidéte locum ubi pósitus erat Dóminus, allelúja, allelúja.');
expect(antiphons).toHaveLength(3);
// No Te Deum -- still the 1-nocturn ferial branch, not promoted to 3.
expect(ordo.parts.some((p) => p.kind === 'te-deum')).toBe(false);
// No stray versicle in the psalmody (same fix as the plain ferial branch).
expect(ordo.parts.slice(5).some((p) => p.kind === 'versicle')).toBe(false);
});
});
describe('resolveOrdo("matins", ...) Duplex+ weekday-feast (3-nocturn, non-Sunday) branch', () => { describe('resolveOrdo("matins", ...) Duplex+ weekday-feast (3-nocturn, non-Sunday) branch', () => {
// 2026-08-17 -- St. Lawrence's own octave closing day ("In Octava S. // 2026-08-17 -- St. Lawrence's own octave closing day ("In Octava S.
// Laurentii Martyris ~ Duplex"), live-verified directly against the // Laurentii Martyris ~ Duplex"), live-verified directly against the