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
it from starting:
- ~~Easter's own octave~~ — DONE (2026-09-01/02): the transfer-cascade piece (calendar
precedence within Holy Week/Easter Day/the Octave/Low Sunday) and Easter Sunday/Monday/
Tuesday's own proper Matins (psalms/antiphons/versicles/canticles, sharing one authored
entry via a new general-purpose temporal-override mechanism) are both built and
- ~~Easter's own octave~~ — DONE (2026-09-01/02): calendar precedence (the transfer-cascade
piece within Holy Week/Easter Day/the Octave/Low Sunday), Easter Sunday/Monday/Tuesday's
own proper Matins, and Wednesday-Saturday's own antiphons are all built, authored, and
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:
Wednesday-Saturday of the Octave's own (different, not yet authored) antiphons.
octave" entries (two general-purpose mechanisms came out of this: a Sunday-branch and a
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
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.
@@ -53,11 +53,6 @@ across every hour and content type in this app.
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
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
`src/data/scripture/` — long-term project, explicitly deferred; Matins scripture
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.
`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 matinsSundayNamedOverridesData from '../data/hours/matins-sunday-named-nocturn-overrides.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';
type BilingualText = Partial<Record<string, string>>;
@@ -227,6 +228,18 @@ interface FerialNocturn {
type MatinsFerialAntiphons = Record<Exclude<Weekday, 'sunday'>, FerialNocturn>;
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;
/** 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');
}
/** 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
* weekday's groups were reconciled against psalter-distribution.yml's own
* "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
* the Capitulum (fixed 2026-09-02, see the data file's own header). */
function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] {
const nocturn = ferialAntiphons[day.weekday as Exclude<Weekday, 'sunday'>];
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[] = [];
for (const group of nocturn.groups) {
const antiphonText = verifiedText(group.antiphon);
@@ -476,6 +498,35 @@ function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] {
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
* entry authored for its winner yet: the plain ferial weekday table,
* 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.');
});
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 day = resolveDay('2026-04-08');
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);
// 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(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', () => {
// 2026-08-17 -- St. Lawrence's own octave closing day ("In Octava S.
// Laurentii Martyris ~ Duplex"), live-verified directly against the