Compare commits
2 Commits
f4d4eb3738
...
57acbeefe9
| Author | SHA1 | Date | |
|---|---|---|---|
| 57acbeefe9 | |||
| c2b24ae77d |
@@ -19,12 +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): the transfer-cascade piece (a candidate
|
||||
impeded within Holy Week/Easter Day/the Octave/Low Sunday now correctly skips the whole
|
||||
span and keeps walking forward, FIFO, until it lands) is built and live-verified — see
|
||||
the dated log's "Easter octave transfer cascade" entry. Content-wise, Easter Day/the
|
||||
Octave's own full Matins proper (distinct psalms, not just antiphons) is still not
|
||||
modeled — a separate, narrower remaining gap, not this one.
|
||||
- ~~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
|
||||
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.
|
||||
- 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.
|
||||
@@ -52,6 +53,11 @@ 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.
|
||||
@@ -4876,3 +4882,55 @@ Terce/Sext/None antiphons+versicles, Te Deum present.
|
||||
See memory `vu_matins_semiduplex_threshold` for full detail.
|
||||
|
||||
`npm test` (823 passed), `tsc --noEmit`, and `npm run build` all pass.
|
||||
|
||||
### Easter's own octave — Sunday/Monday/Tuesday proper Matins authored, general temporal-override mechanism added (2026-09-02)
|
||||
|
||||
Closes most of the long-tabled "Easter's own octave" mechanism gap (Priority section, item 1).
|
||||
User pushed back on an initial overcomplicated read ("psalms with lections" looked like a new
|
||||
rendering shape at first glance) — turned out, after live-verifying the actual structure, to
|
||||
be exactly what the user guessed: a temporal override file that "looks weird" only because of
|
||||
its content (13 psalms under one antiphon per nocturn, not the usual per-pair grouping), not
|
||||
because it needs new mechanism. Every psalm already gets its own Gloria Patri centrally
|
||||
(`hours/index.ts`'s `withGloriaPatri`) regardless of hour, and the 12 patristic lessons were
|
||||
*already* authored (`data/propers/nocturn-readings/easter-sunday.yml`, from the earlier
|
||||
temporal nocturn-readings sweep) — the only real gap was the psalmody/antiphon/versicle/
|
||||
canticle layer.
|
||||
|
||||
**Content**: new `easter-sunday` entry in `matins-sunday-named-nocturn-overrides.yml` —
|
||||
live-verified against the reference engine (`Pofficium.pl`, `prayMatutinum`, Monastic
|
||||
Tridentinum 1617, 2026-04-05/06/07) — Nocturn 1: psalms 1,2,8,15,23,27 under one shared
|
||||
antiphon ("Ego sum qui sum"); Nocturn 2: psalms 29,63,65,75,87,107 under one shared antiphon
|
||||
("Terra trémuit"); Nocturn 3: the same 3 canticles already authored for the ordinary-
|
||||
Paschaltide seasonal override (Isa 63:1-5, Osee 6:1-6, Soph 3:8-13), under its own antiphon
|
||||
("Nolíte expavéscere"). All 3 nocturn-closing versicles are byte-identical to the ones
|
||||
already authored in `matins-sunday-paschaltide-nocturn-overrides.yml`, confirming those are
|
||||
shared season-wide rather than coincidentally reused.
|
||||
|
||||
**Mechanism (small, general-purpose, not Easter-specific)**: `hours/matins.ts`'s
|
||||
`threeNocturns` gate and the Sunday-branch dispatch previously only ever fired for
|
||||
`day.weekday === 'sunday'` or a sanctoral winner — a *temporal* winner had no path into 3-
|
||||
nocturn proper content at all, even with a matching `sundayNamedOverrides` entry. Generalized:
|
||||
a temporal winner with an authored named override is now eligible too (`hasTemporalNamedOverride`),
|
||||
reusing the exact same `sundayPsalmNocturn`/`sundayCanticleNocturn` render functions the Sunday
|
||||
and saint-proper branches already share — deliberately built as a reusable feature (user,
|
||||
2026-09-02: "we want temporal override anyway... there are temporal feasts that could have
|
||||
their own psalms/lessons"), not a one-off hack.
|
||||
|
||||
**The scoping wrinkle that made this non-trivial**: `winner.id` is `'easter-sunday'` for the
|
||||
*entire* Octave (all 6 weekdays), but only Sunday/Monday/Tuesday are live-verified to share
|
||||
this content — Wednesday-Saturday have their own different (Paschal-alleluia-flavored, not yet
|
||||
authored) antiphons over a differently-shaped psalm set. A naive "any day with this winner.id"
|
||||
check would have wrongly applied Sunday's content through Saturday. Fixed with a new optional
|
||||
`weekdays` field on a named-override entry (`easter-sunday: weekdays: [sunday, monday,
|
||||
tuesday]`) — omitted (as `sunday-after-ascension` still does) means "every day carrying this
|
||||
temporalId," the common case for a temporalId that already maps to exactly one calendar day.
|
||||
|
||||
**Still open, a separate smaller gap**: Wednesday-Saturday of the Easter Octave — live-verified
|
||||
(2026-04-08) to carry real Paschal-alleluia antiphons ("Jesum qui crucifíxus est quǽritis,
|
||||
allelúja...") over a psalm set the reference engine sizes differently from vu's own
|
||||
already-fixed 9-psalm ferial table (itself deliberate — see "No RB summer contraction" in
|
||||
CLAUDE.md). 4 more days of antiphon-only content, same shape as the existing Advent/Paschaltide
|
||||
seasonal-overlay pattern, layered onto vu's own ferial psalm numbers rather than trying to
|
||||
reproduce the reference engine's differently-sized set — not started.
|
||||
|
||||
`npm test` (828 passed), `tsc --noEmit`, and `npm run build` all pass.
|
||||
|
||||
@@ -7,6 +7,14 @@
|
||||
#
|
||||
# hours/matins.ts's lookup checks this map first (most specific), ahead of
|
||||
# the season-wide overrides, when `temporalId` matches a key here.
|
||||
#
|
||||
# `easter-sunday` (below) is also reused for Easter Monday and Tuesday
|
||||
# (`hours/matins.ts`'s threeNocturns/Sunday-branch dispatch route any
|
||||
# temporal winner with a matching entry here through the same rendering as
|
||||
# a real Sunday, not just `day.weekday === 'sunday'` literally) — the
|
||||
# reference engine's own content is byte-identical across all three days
|
||||
# (live-verified 2026-09-02, 2026-04-05/06/07), so one entry covers all
|
||||
# three rather than three separate near-duplicate ones.
|
||||
|
||||
# Sunday within the Octave of Ascension (`sunday-after-ascension`) —
|
||||
# live-verified against the local reference-engine CGI instance
|
||||
@@ -32,6 +40,71 @@
|
||||
# translation of the underlying Mark 16:20 text (a well-known, unambiguous
|
||||
# formula, not a guess), consistent with how every other antiphon in this
|
||||
# file is sourced.
|
||||
# Easter Sunday itself (and Monday/Tuesday of the Octave, which share this
|
||||
# entry — see the file header above). Live-verified against the local
|
||||
# reference-engine CGI instance (`prayMatutinum`, Monastic Tridentinum
|
||||
# 1617, 2026-09-02, proof dates 2026-04-05/06/07). Unlike every other
|
||||
# Sunday scheme in this file, each nocturn here uses ONE shared antiphon
|
||||
# over ALL 6 of its psalms (not one antiphon per psalm, nor per pair) —
|
||||
# confirmed by the live query: a single "Ant." before the first psalm and
|
||||
# the same antiphon repeated in full after the last, nothing in between.
|
||||
# Nocturn III reuses the same 3 canticles as the ordinary-Paschaltide
|
||||
# override (Isa 63:1-5, Osee 6:1-6, Sophonias 3:8-13 — already authored,
|
||||
# no new scripture files needed) under its own different antiphon; all 3
|
||||
# nocturn-closing versicles are also byte-identical to the ones already
|
||||
# authored in matins-sunday-paschaltide-nocturn-overrides.yml, confirming
|
||||
# those are shared season-wide, not coincidentally reused.
|
||||
#
|
||||
# The reference engine's own [Rule] for this day actually reads "1
|
||||
# nocturn" (the whole night office collapses to a single nocturn of 3
|
||||
# lessons on this most privileged of feasts) — not modeled that way here;
|
||||
# this app's own threeNocturns rule renders every Sunday with 3 nocturns
|
||||
# unconditionally regardless of the historical single-nocturn rubric for
|
||||
# this particular feast (same convention already noted in
|
||||
# nocturn-readings/easter-sunday.yml's own header for the reading side).
|
||||
#
|
||||
# Explicitly NOT covered by this entry: Wednesday-Saturday of the Octave
|
||||
# have their own real Paschal-alleluia antiphons layered over (in the
|
||||
# reference engine) a differently-sized psalm set — live-verified present
|
||||
# (2026-04-08, Wednesday) but not authored here. A separate, smaller gap
|
||||
# (4 more days of antiphon-only content, analogous to the existing Advent/
|
||||
# Paschaltide seasonal-overlay shape) — see TODO.md.
|
||||
easter-sunday:
|
||||
# winner.id 'easter-sunday' spans the whole Octave (all 6 weekdays), but
|
||||
# only these 3 calendar days are live-verified to share this content —
|
||||
# restricting the entry so Wednesday-Saturday keep falling through to
|
||||
# their own (not yet authored) path instead of wrongly reusing this one.
|
||||
weekdays: [sunday, monday, tuesday]
|
||||
nocturn1:
|
||||
groups:
|
||||
- psalms: [1, 2, 8, 15, 23, 27]
|
||||
antiphon:
|
||||
la: "Ego sum qui sum, * et consílium meum non est cum ímpiis, sed in lege Dómini volúntas mea est, allelúja."
|
||||
en: "I am who I am, * and my counsel is not with the ungodly, my will is in the law of God, alleluia."
|
||||
versicle:
|
||||
v: { la: "Surréxit Dóminus de sepúlcro, allelúja.", en: "The Lord is risen from the sepulchre, alleluia." }
|
||||
r: { la: "Qui pro nobis pepéndit in ligno, allelúja.", en: "Who for us was hanged on the tree, alleluia." }
|
||||
nocturn2:
|
||||
groups:
|
||||
- psalms: [29, 63, 65, 75, 87, 107]
|
||||
antiphon:
|
||||
la: "Terra * trémuit et quiévit, † dum exsúrgeret in judício Deus, allelúja."
|
||||
en: "The earth * trembled and was still, when God arose to judgment, alleluia."
|
||||
versicle:
|
||||
v: { la: "Surréxit Dóminus vere, allelúja.", en: "The Lord is risen indeed, alleluia." }
|
||||
r: { la: "Et appáruit Simóni, allelúja.", en: "And hath appeared to Simon, alleluia." }
|
||||
nocturn3:
|
||||
canticles:
|
||||
- refs: [{ book: isa, chapter: 63, verses: "1-5" }]
|
||||
- refs: [{ book: osee, chapter: 6, verses: "1-6" }]
|
||||
- refs: [{ book: soph, chapter: 3, verses: "8-13" }]
|
||||
antiphon:
|
||||
la: "Nolíte expavéscere: * Jesum quǽritis Nazarénum, crucifíxum: surréxit, non est hic, allelúja."
|
||||
en: "Be not affrighted; * you seek Jesus of Nazareth, who was crucified: he is risen, he is not here, alleluia."
|
||||
versicle:
|
||||
v: { la: "Gavísi sunt discípuli, allelúja.", en: "The disciples were glad, alleluia." }
|
||||
r: { la: "Viso Dómino, allelúja.", en: "When they saw the Lord, alleluia." }
|
||||
|
||||
sunday-after-ascension:
|
||||
nocturn1:
|
||||
groups:
|
||||
|
||||
+36
-13
@@ -170,11 +170,23 @@ const sundayPaschaltideOverrides = matinsSundayPaschaltideOverridesData as unkno
|
||||
|
||||
// A specific privileged Sunday whose own proper is wholesale different
|
||||
// from the fixed 12-psalm/3-canticle scheme, keyed by `temporalId` — see
|
||||
// matins-sunday-named-nocturn-overrides.yml's own header (currently just
|
||||
// `sunday-after-ascension`). Checked ahead of the season-wide overrides
|
||||
// above, though in practice they never collide (no season-wide override
|
||||
// exists for `ascensiontide`).
|
||||
const sundayNamedOverrides = matinsSundayNamedOverridesData as unknown as Record<string, Partial<MatinsSundayNocturnOverrides>>;
|
||||
// matins-sunday-named-nocturn-overrides.yml's own header (currently
|
||||
// `sunday-after-ascension` and `easter-sunday`). Checked ahead of the
|
||||
// season-wide overrides above, though in practice they never collide (no
|
||||
// season-wide override exists for `ascensiontide`).
|
||||
//
|
||||
// `weekdays` (2026-09-02): optional, for a `temporalId` that spans more
|
||||
// calendar days than actually share this content — e.g. `easter-sunday`
|
||||
// is the winner id for the *whole* Easter Octave (all 6 weekdays), but
|
||||
// only Sunday/Monday/Tuesday of it are live-verified to share this exact
|
||||
// proper; Wednesday-Saturday have their own different (not yet authored)
|
||||
// antiphons. Omitted entirely (as `sunday-after-ascension` does) means
|
||||
// "every day carrying this temporalId" — the common case for a temporal
|
||||
// override whose id already maps to exactly one calendar day.
|
||||
interface MatinsSundayNamedOverride extends Partial<MatinsSundayNocturnOverrides> {
|
||||
weekdays?: Weekday[];
|
||||
}
|
||||
const sundayNamedOverrides = matinsSundayNamedOverridesData as unknown as Record<string, MatinsSundayNamedOverride>;
|
||||
|
||||
/** Sunday Nocturn I/II/III content for a given day — a per-temporalId
|
||||
* named override (a specific privileged Sunday's own wholesale-different
|
||||
@@ -734,17 +746,28 @@ export function resolveOrdo(date: string): ResolvedOrdo {
|
||||
const day = resolveDay(date);
|
||||
const winner = resolveOfficeWinner(day);
|
||||
const temporalId = resolveTemporalId(date);
|
||||
// Every Sunday, unconditionally, or a Semiduplex+ sanctoral winner — the
|
||||
// Every Sunday, unconditionally, a Semiduplex+ sanctoral winner — the
|
||||
// user's own chosen threshold (2026-08, lowered from Duplex to
|
||||
// Semiduplex 2026-09-02), not gated on whether any content is actually
|
||||
// authored yet, same "eligible, not content-gated" convention every
|
||||
// other per-feast override in this app already uses (see
|
||||
// hours/resolve-common.ts's getOfficeOverrideId). Deliberately a
|
||||
// separate check from `isDoubleOrHigher` (antiphon.ts) — that function
|
||||
// drives the unrelated antiphon-doubling rule (full vs. incipit antiphon
|
||||
// before the psalms) and stays floored at Duplex; the two thresholds
|
||||
// only coincided before this change by accident, not by shared meaning.
|
||||
const threeNocturns = day.weekday === 'sunday' || (winner.kind === 'sanctoral' && isAtLeast(winner.rank, 'semiduplex'));
|
||||
// hours/resolve-common.ts's getOfficeOverrideId) — or a temporal winner
|
||||
// with its own named override (2026-09-02, e.g. Easter Monday/Tuesday
|
||||
// reusing Easter Sunday's own entry; see `hasTemporalNamedOverride`
|
||||
// below). Unlike the sanctoral case, a plain temporal id is NOT
|
||||
// eligible just by existing — only one with an authored entry in
|
||||
// `sundayNamedOverrides` is, since ordinary temporal ferias/Sundays
|
||||
// already have their own correct path and shouldn't suddenly gain 3
|
||||
// nocturns. Deliberately a separate check from `isDoubleOrHigher`
|
||||
// (antiphon.ts) — that function drives the unrelated antiphon-doubling
|
||||
// rule (full vs. incipit antiphon before the psalms) and stays floored
|
||||
// at Duplex; the two thresholds only coincided before this change by
|
||||
// accident, not by shared meaning.
|
||||
const temporalNamedOverride = winner.kind === 'temporal' ? sundayNamedOverrides[winner.id] : undefined;
|
||||
const hasTemporalNamedOverride =
|
||||
temporalNamedOverride !== undefined && (temporalNamedOverride.weekdays === undefined || temporalNamedOverride.weekdays.includes(day.weekday));
|
||||
const threeNocturns =
|
||||
day.weekday === 'sunday' || (winner.kind === 'sanctoral' && isAtLeast(winner.rank, 'semiduplex')) || hasTemporalNamedOverride;
|
||||
const pool = buildReadingPool(day, temporalId, date, threeNocturns);
|
||||
const [nocturn1Readings, nocturn2Readings, nocturn3Readings] = distributeIntoNocturns(pool, threeNocturns ? 3 : 1);
|
||||
|
||||
@@ -767,7 +790,7 @@ export function resolveOrdo(date: string): ResolvedOrdo {
|
||||
{ kind: 'hymn', text: resolveMatinsHymn(day) },
|
||||
];
|
||||
|
||||
if (threeNocturns && day.weekday === 'sunday') {
|
||||
if (threeNocturns && (day.weekday === 'sunday' || hasTemporalNamedOverride)) {
|
||||
parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[0]}` });
|
||||
parts.push(...sundayPsalmNocturn(sundayNocturnFor('nocturn1', day, temporalId), day));
|
||||
parts.push(...(nocturn1Readings ?? []));
|
||||
|
||||
@@ -360,6 +360,54 @@ describe('resolveOrdo("matins", ...) Sunday within the Octave of Ascension (2026
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveOrdo("matins", ...) Easter Sunday/Monday/Tuesday shared proper Matins (2026-09-02 fix)', () => {
|
||||
// All 6 weekdays of the Easter Octave share winner {kind: 'temporal', id:
|
||||
// 'easter-sunday'} -- only Sunday/Monday/Tuesday are live-verified to
|
||||
// share this exact proper content, restricted via the override's own
|
||||
// `weekdays` field (see matins-sunday-named-nocturn-overrides.yml).
|
||||
const EASTER_WEEK_2026 = ['2026-04-05', '2026-04-06', '2026-04-07'] as const;
|
||||
|
||||
it.each(EASTER_WEEK_2026)('%s shares the identical 13-psalm proper scheme, 3 nocturns, Te Deum', (date) => {
|
||||
const day = resolveDay(date);
|
||||
expect(day.winner).toEqual({ kind: 'temporal', id: 'easter-sunday' });
|
||||
const ordo = resolveOrdo('matins', date);
|
||||
const psalms = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
||||
expect(psalms).toEqual([3, 94, 1, 2, 8, 15, 23, 27, 29, 63, 65, 75, 87, 107]);
|
||||
expect(ordo.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
||||
|
||||
const canticles = ordo.parts.filter((p) => p.kind === 'canticle') as { canticleId: string }[];
|
||||
expect(canticles.map((c) => c.canticleId)).toEqual(['isa-63-1-5', 'osee-6-1-6', 'soph-3-8-13']);
|
||||
|
||||
const antiphons = ordo.parts.filter((p) => p.kind === 'antiphon') as { text: { text: Record<string, string> } }[];
|
||||
expect(antiphons[1]?.text.text.la).toBe('Ant. Ego sum qui sum, * et consílium meum non est cum ímpiis, sed in lege Dómini volúntas mea est, allelúja.');
|
||||
expect(antiphons.at(-1)?.text.text.la).toBe(
|
||||
'Ant. Nolíte expavéscere: * Jesum quǽritis Nazarénum, crucifíxum: surréxit, non est hic, allelúja.',
|
||||
);
|
||||
|
||||
const versicles = ordo.parts.filter((p) => p.kind === 'versicle') as { text: { text: Record<string, string> } }[];
|
||||
const [, , nocturn1, nocturn2, nocturn3] = versicles;
|
||||
expect(nocturn1?.text.text.la).toBe('V. Surréxit Dóminus de sepúlcro, allelúja.\nR. Qui pro nobis pepéndit in ligno, allelúja.');
|
||||
expect(nocturn2?.text.text.la).toBe('V. Surréxit Dóminus vere, allelúja.\nR. Et appáruit Simóni, 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', () => {
|
||||
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.
|
||||
expect(psalms.slice(2)).toEqual(getPsalmsFor('matins', 'wednesday').map((r) => r.number));
|
||||
expect(ordo.parts.some((p) => p.kind === 'te-deum')).toBe(false);
|
||||
});
|
||||
|
||||
it('Low Sunday (a different temporalId) is unaffected, still the plain fixed Sunday scheme', () => {
|
||||
const ordo = resolveOrdo('matins', '2026-04-12');
|
||||
const psalms = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
||||
expect(psalms).toEqual([3, 94, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]);
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user