Compare commits

..

2 Commits

Author SHA1 Message Date
will 57acbeefe9 Author Easter Sunday/Monday/Tuesday's shared proper Matins
Deploy / deploy (push) Successful in 1m29s
Easter Sunday, Monday, and Tuesday of the Octave share winner {kind:
temporal, id: easter-sunday}, and now share one live-verified proper
Matins psalmody: Nocturn 1 (Ps 1,2,8,15,23,27 under one antiphon),
Nocturn 2 (Ps 29,63,65,75,87,107 under one antiphon), Nocturn 3 (the
same 3 canticles already authored for the ordinary-Paschaltide
seasonal override, under its own antiphon) — sourced from
Pofficium.pl (Monastic Tridentinum 1617, prayMatutinum) against
2026-04-05/06/07. All 3 nocturn-closing versicles are byte-identical
to the ones already authored for the Paschaltide override, confirming
those are shared season-wide rather than coincidentally reused. The
12 patristic lessons were already authored in an earlier sweep
(nocturn-readings/easter-sunday.yml) and needed no new work.

Restricted to just these 3 days via the new weekdays field: the
Octave's other 4 weekdays (Wed-Sat) share the same winner id but have
their own different, not-yet-authored antiphons, tracked as a
separate open item in TODO.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X26BkFT3ARbtUGXuBmzUmD
2026-09-02 07:42:39 -04:00
will c2b24ae77d Let a temporal winner reach Matins' 3-nocturn proper psalmody
hours/matins.ts's threeNocturns gate and Sunday-branch dispatch only
ever fired for a literal Sunday or a sanctoral winner — a temporal
winner (e.g. Easter Monday/Tuesday, sharing Easter Sunday's own
winner id) had no path into 3-nocturn proper content at all, even
with a matching named override authored for it.

Generalizes hours/matins-sunday-named-nocturn-overrides.yml's lookup
so a temporal winner with an authored entry is eligible too, reusing
the same sundayPsalmNocturn/sundayCanticleNocturn render functions
the Sunday and saint-proper branches already share. A new optional
weekdays field lets one entry restrict itself to specific days of a
temporalId that spans more calendar days than actually share its
content (needed by the Easter content this unblocks, added next).

Built as a general-purpose feature, not scoped to Easter specifically
— any temporal feast that wants its own proper psalms/lessons can now
reach this path by adding a named-override entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X26BkFT3ARbtUGXuBmzUmD
2026-09-02 07:42:06 -04:00
4 changed files with 221 additions and 19 deletions
+64 -6
View File
@@ -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 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): the transfer-cascade piece (a candidate - ~~Easter's own octave~~ — DONE (2026-09-01/02): the transfer-cascade piece (calendar
impeded within Holy Week/Easter Day/the Octave/Low Sunday now correctly skips the whole precedence within Holy Week/Easter Day/the Octave/Low Sunday) and Easter Sunday/Monday/
span and keeps walking forward, FIFO, until it lands) is built and live-verified — see Tuesday's own proper Matins (psalms/antiphons/versicles/canticles, sharing one authored
the dated log's "Easter octave transfer cascade" entry. Content-wise, Easter Day/the entry via a new general-purpose temporal-override mechanism) are both built and
Octave's own full Matins proper (distinct psalms, not just antiphons) is still not live-verified — see the dated log's "Easter octave transfer cascade" and "Easter's own
modeled — a separate, narrower remaining gap, not this one. 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 - 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.
@@ -52,6 +53,11 @@ 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.
@@ -4876,3 +4882,55 @@ Terce/Sext/None antiphons+versicles, Te Deum present.
See memory `vu_matins_semiduplex_threshold` for full detail. See memory `vu_matins_semiduplex_threshold` for full detail.
`npm test` (823 passed), `tsc --noEmit`, and `npm run build` all pass. `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 # hours/matins.ts's lookup checks this map first (most specific), ahead of
# the season-wide overrides, when `temporalId` matches a key here. # 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`) — # Sunday within the Octave of Ascension (`sunday-after-ascension`) —
# live-verified against the local reference-engine CGI instance # 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 # translation of the underlying Mark 16:20 text (a well-known, unambiguous
# formula, not a guess), consistent with how every other antiphon in this # formula, not a guess), consistent with how every other antiphon in this
# file is sourced. # 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: sunday-after-ascension:
nocturn1: nocturn1:
groups: groups:
+36 -13
View File
@@ -170,11 +170,23 @@ const sundayPaschaltideOverrides = matinsSundayPaschaltideOverridesData as unkno
// A specific privileged Sunday whose own proper is wholesale different // A specific privileged Sunday whose own proper is wholesale different
// from the fixed 12-psalm/3-canticle scheme, keyed by `temporalId` — see // from the fixed 12-psalm/3-canticle scheme, keyed by `temporalId` — see
// matins-sunday-named-nocturn-overrides.yml's own header (currently just // matins-sunday-named-nocturn-overrides.yml's own header (currently
// `sunday-after-ascension`). Checked ahead of the season-wide overrides // `sunday-after-ascension` and `easter-sunday`). Checked ahead of the
// above, though in practice they never collide (no season-wide override // season-wide overrides above, though in practice they never collide (no
// exists for `ascensiontide`). // season-wide override exists for `ascensiontide`).
const sundayNamedOverrides = matinsSundayNamedOverridesData as unknown as Record<string, Partial<MatinsSundayNocturnOverrides>>; //
// `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 /** Sunday Nocturn I/II/III content for a given day — a per-temporalId
* named override (a specific privileged Sunday's own wholesale-different * 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 day = resolveDay(date);
const winner = resolveOfficeWinner(day); const winner = resolveOfficeWinner(day);
const temporalId = resolveTemporalId(date); 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 // user's own chosen threshold (2026-08, lowered from Duplex to
// Semiduplex 2026-09-02), not gated on whether any content is actually // Semiduplex 2026-09-02), not gated on whether any content is actually
// authored yet, same "eligible, not content-gated" convention every // authored yet, same "eligible, not content-gated" convention every
// other per-feast override in this app already uses (see // other per-feast override in this app already uses (see
// hours/resolve-common.ts's getOfficeOverrideId). Deliberately a // hours/resolve-common.ts's getOfficeOverrideId) — or a temporal winner
// separate check from `isDoubleOrHigher` (antiphon.ts) — that function // with its own named override (2026-09-02, e.g. Easter Monday/Tuesday
// drives the unrelated antiphon-doubling rule (full vs. incipit antiphon // reusing Easter Sunday's own entry; see `hasTemporalNamedOverride`
// before the psalms) and stays floored at Duplex; the two thresholds // below). Unlike the sanctoral case, a plain temporal id is NOT
// only coincided before this change by accident, not by shared meaning. // eligible just by existing — only one with an authored entry in
const threeNocturns = day.weekday === 'sunday' || (winner.kind === 'sanctoral' && isAtLeast(winner.rank, 'semiduplex')); // `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 pool = buildReadingPool(day, temporalId, date, threeNocturns);
const [nocturn1Readings, nocturn2Readings, nocturn3Readings] = distributeIntoNocturns(pool, threeNocturns ? 3 : 1); const [nocturn1Readings, nocturn2Readings, nocturn3Readings] = distributeIntoNocturns(pool, threeNocturns ? 3 : 1);
@@ -767,7 +790,7 @@ export function resolveOrdo(date: string): ResolvedOrdo {
{ kind: 'hymn', text: resolveMatinsHymn(day) }, { 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({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[0]}` });
parts.push(...sundayPsalmNocturn(sundayNocturnFor('nocturn1', day, temporalId), day)); parts.push(...sundayPsalmNocturn(sundayNocturnFor('nocturn1', day, temporalId), day));
parts.push(...(nocturn1Readings ?? [])); parts.push(...(nocturn1Readings ?? []));
+48
View File
@@ -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', () => { 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