Add Matins psalmody overrides for a Duplex+ weekday feast
matins.ts's three-nocturn gate correctly governed nocturn count (Sunday, or Duplex+) but wrongly reused the literal Sunday psalmody as content for every three-nocturn day, including a weekday feast. Live- verified (Monastic Tridentinum 1617) that a Duplex+ weekday feast has its own genuinely different psalmody -- confirmed per-Common-category, not per-saint, by cross-checking St. Lawrence against St. Ignatius of Antioch (a different Martyr sub-category, nearly identical psalm numbers, different antiphons). New mechanism (matins-psalmody-overrides.ts, mirroring the Lauds one but keyed per-category) with St. Lawrence authored as the live-verified proof; falls back to the plain ferial weekday table, redistributed into 3 nocturns, for every other Duplex+ id until its own category's content is authored. Also generalizes the canticle shape to support a citation spanning multiple scripture chapters under one heading (Lawrence's own Nocturn 3), and adds the new Sirach/Jeremiah chapters his canticles cite.
This commit is contained in:
@@ -0,0 +1,118 @@
|
|||||||
|
# Verified against Divinum Officium (Monastic Tridentinum 1617), live
|
||||||
|
# prayMatutinum query (2026-08-17, St. Lawrence's own octave closing day
|
||||||
|
# -- "In Octava S. Laurentii Martyris ~ Duplex"). This is the live-
|
||||||
|
# verified proof for the Matins-psalmody-override mechanism
|
||||||
|
# (hours/matins-psalmody-overrides.ts) -- the general mechanism gap this
|
||||||
|
# closes: a Duplex+ weekday feast previously fell through to reusing the
|
||||||
|
# literal *Sunday* psalmody (data/hours/matins-sunday-antiphons.yml, Ps
|
||||||
|
# 20-31) unconditionally, which is wrong -- a Duplex+ weekday feast has
|
||||||
|
# its own real, different psalmody (confirmed against the live engine
|
||||||
|
# directly), not the Sunday's.
|
||||||
|
#
|
||||||
|
# Cross-checked against a second Duplex saint (St. Ignatius of Antioch,
|
||||||
|
# Common of a Martyr-Bishop, no proper content of his own, live-queried
|
||||||
|
# 2029-02-01): Nocturn 1 is *identical* (Ps 1,2,4,5,8,10); Nocturn 2 is
|
||||||
|
# nearly identical (Ps 14,20,23,63,91 match -- only one slot differs,
|
||||||
|
# Lawrence has 16 where Ignatius has 64). So the psalm *numbers* are a
|
||||||
|
# shared per-Common-category pool, not unique to Lawrence -- only the
|
||||||
|
# antiphon *text* is proper to him (each one narrates an episode of his
|
||||||
|
# own martyrdom, e.g. "Quo progréderis sine fílio, pater?"). This file is
|
||||||
|
# keyed by his own saint id (not yet a Common-category id) since only his
|
||||||
|
# own proper antiphons have been authored so far -- see this module's own
|
||||||
|
# doc comment for what extending this to a real Common-category key would
|
||||||
|
# need.
|
||||||
|
#
|
||||||
|
# Nocturn 3's first canticle spans two Sirach chapters under one heading
|
||||||
|
# in the source ("Eccli 14:22;15:3-4;15:6") -- kept as one canticle with
|
||||||
|
# two refs (matins.ts's own `canticles: { refs: [...] }[]` shape), not
|
||||||
|
# split into two, matching how the live engine presents it. New scripture
|
||||||
|
# chapters authored for this: data/scripture/{sir-14,sir-15,jer-17,sir-31}.yml
|
||||||
|
# (all sparse, only the cited verses -- same convention as sir-36.yml's
|
||||||
|
# own Sunday-canticle transcription).
|
||||||
|
id: st-lawrence
|
||||||
|
nocturn1:
|
||||||
|
groups:
|
||||||
|
- psalms: [1]
|
||||||
|
antiphon:
|
||||||
|
la: "Quo progréderis * sine fílio, pater? quo, sacérdos sancte, sine minístro próperas?"
|
||||||
|
en: "Father, whither goest thou * without thy son? Holy Priest, dost thou fare hence without a Deacon?"
|
||||||
|
- psalms: [2]
|
||||||
|
antiphon:
|
||||||
|
la: "Noli me derelínquere, * pater sancte, quia thesáuros tuos jam expéndi, quos tradidísti mihi."
|
||||||
|
en: "Leave me not, holy father, * for I have spent already thy treasures, which thou gavest unto me."
|
||||||
|
- psalms: [4]
|
||||||
|
antiphon:
|
||||||
|
la: "Non ego te désero, * fili, neque derelínquo; sed majóra tibi debéntur pro Christi fide certámina."
|
||||||
|
en: "I leave thee not, my son, neither forsake thee; * there awaiteth thee for Christ's truth a sterner wrestling than mine."
|
||||||
|
- psalms: [5]
|
||||||
|
antiphon:
|
||||||
|
la: "Beátus Lauréntius * orábat, dicens: Dómine Jesu Christe, Deus de Deo, miserére mihi servo tuo."
|
||||||
|
en: "The blessed Lawrence prayed * and said: O Lord Jesus Christ, God of God, take pity upon me thy servant."
|
||||||
|
- psalms: [8]
|
||||||
|
antiphon:
|
||||||
|
la: "Dixit Románus * ad beátum Lauréntium: Vídeo ante te júvenem pulchérrimum, festína me baptizáre."
|
||||||
|
en: "Romanus said * to Blessed Lawrence: I see in front of thee a very well-favoured young man; make haste to baptize me."
|
||||||
|
- psalms: [10]
|
||||||
|
antiphon:
|
||||||
|
la: "Beátus Lauréntius * dixit: Mea nox obscúrum non habet, sed ómnia in luce claréscunt."
|
||||||
|
en: "The blessed Lawrence said: The darkness is no darkness to me, * but the night is all as clear as morning, that shineth more and more unto the perfect day."
|
||||||
|
versicle:
|
||||||
|
v:
|
||||||
|
la: "Glória et honóre coronásti eum, Dómine."
|
||||||
|
en: "Thou hast crowned him with glory and honour, O Lord."
|
||||||
|
r:
|
||||||
|
la: "Et constituísti eum super ópera mánuum tuárum."
|
||||||
|
en: "And madest him to have dominion over the works of thy hands."
|
||||||
|
nocturn2:
|
||||||
|
groups:
|
||||||
|
- psalms: [14]
|
||||||
|
antiphon:
|
||||||
|
la: "Strinxérunt * córporis membra pósita super cratículam: subiciéntibus prunas insúltat Levíta Christi."
|
||||||
|
en: "They laid him upon the grating, and stretched out his limbs; * Christ's Levite held in derision them that brought the live coals."
|
||||||
|
- psalms: [16]
|
||||||
|
antiphon:
|
||||||
|
la: "Igne me examinásti, * et non est invénta in me iníquitas."
|
||||||
|
en: "Thou hast tried me with fire, * and found no wickedness in me."
|
||||||
|
- psalms: [20]
|
||||||
|
antiphon:
|
||||||
|
la: "Interrogátus * te Dóminum conféssus sum, assátus grátias ago."
|
||||||
|
en: "When I was asked, I acknowledged thee to be the Lord; now that I am roasted, I give thee thanks."
|
||||||
|
- psalms: [23]
|
||||||
|
antiphon:
|
||||||
|
la: "Beátus Lauréntius * clamávit et dixit: Deum meum colo, et illi solo sérvio."
|
||||||
|
en: "Blessed Lawrence * cried out and said: I care for my God, and serve him alone."
|
||||||
|
- psalms: [63]
|
||||||
|
antiphon:
|
||||||
|
la: "Dispérsit * dedit paupéribus: justítia ejus manet in séculum sǽculi."
|
||||||
|
en: "He hath dispersed and gave to the poor; * his justice shall remain in ages of ages."
|
||||||
|
- psalms: [91]
|
||||||
|
antiphon:
|
||||||
|
la: "Protéctor tuus * ego sum noli timére: si transíeris per ignem, flamma non nocébit tibi, et odor ignis non erit in te."
|
||||||
|
en: "I am thy protector, do not fear; * if you pass through fire, the flame will not harm thee, and the scent of fire will not be upon thee."
|
||||||
|
versicle:
|
||||||
|
v:
|
||||||
|
la: "Posuísti, Dómine, super caput ejus."
|
||||||
|
en: "O Lord, Thou hast set a crown of precious stones."
|
||||||
|
r:
|
||||||
|
la: "Corónam de lápide pretióso."
|
||||||
|
en: "Upon his head."
|
||||||
|
nocturn3:
|
||||||
|
antiphon:
|
||||||
|
la: "Nisi granum fruménti * cadens, in terram mórtuum fúerit, ipsum solum manet."
|
||||||
|
en: "Unless the grain of wheat * falls to the ground and dies, it only remains as such."
|
||||||
|
canticles:
|
||||||
|
- refs:
|
||||||
|
- { book: sir, chapter: 14, verses: "22" }
|
||||||
|
- { book: sir, chapter: 15, verses: "3-4" }
|
||||||
|
- { book: sir, chapter: 15, verses: "6" }
|
||||||
|
- refs:
|
||||||
|
- { book: jer, chapter: 17, verses: "7-8" }
|
||||||
|
- refs:
|
||||||
|
- { book: sir, chapter: 31, verses: "8-11" }
|
||||||
|
versicle:
|
||||||
|
v:
|
||||||
|
la: "Magna est glória ejus in salutári tuo."
|
||||||
|
en: "His glory is great in thy salvation."
|
||||||
|
r:
|
||||||
|
la: "Glóriam et magnum decórem impónes super eum."
|
||||||
|
en: "Honour and great majesty shalt Thou lay upon him."
|
||||||
@@ -62,9 +62,9 @@ nocturn2:
|
|||||||
|
|
||||||
nocturn3:
|
nocturn3:
|
||||||
canticles:
|
canticles:
|
||||||
- { book: isa, chapter: 33, verses: "2-10" }
|
- refs: [{ book: isa, chapter: 33, verses: "2-10" }]
|
||||||
- { book: isa, chapter: 33, verses: "13-18" }
|
- refs: [{ book: isa, chapter: 33, verses: "13-18" }]
|
||||||
- { book: sir, chapter: 36, verses: "14-19" }
|
- refs: [{ book: sir, chapter: 36, verses: "14-19" }]
|
||||||
antiphon:
|
antiphon:
|
||||||
la: "Allelúja, allelúja, allelúja."
|
la: "Allelúja, allelúja, allelúja."
|
||||||
en: "Alleluia, alleluia, alleluia."
|
en: "Alleluia, alleluia, alleluia."
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Jeremiah 17:7-8, transcribed from a live Divinum Officium query
|
||||||
|
# (Monastic Tridentinum 1617, command=prayMatutinum, 2026-08-17 — St.
|
||||||
|
# Lawrence's own octave closing day, Matins Nocturn III's second
|
||||||
|
# canticle). Verse 8 is split into two half-verses by the source itself
|
||||||
|
# (each half its own antiphon-verse line); transcribed as two separate
|
||||||
|
# entries under the same verse number, matching how the source renders
|
||||||
|
# it, not merged into one. Only 7-8 are authored; the rest of the
|
||||||
|
# chapter is unauthored.
|
||||||
|
book: jer
|
||||||
|
chapter: 17
|
||||||
|
verses:
|
||||||
|
- n: 7
|
||||||
|
text:
|
||||||
|
la: "Benedíctus vir qui confídit in Dómino, * et erit Dóminus fidúcia ejus."
|
||||||
|
en: "Blessed be the man that trusteth in the Lord, * and the Lord shall be his confidence."
|
||||||
|
status: { la: verified, en: verified }
|
||||||
|
- n: 8
|
||||||
|
text:
|
||||||
|
la: "Et erit quasi lignum quod transplantátur super aquas, quod ad humórem mittit radíces suas, * et non timébit cum vénerit æstus."
|
||||||
|
en: "And he shall be as a tree that is planted by the waters, that spreadeth out its roots towards moisture: * and it shall not fear when the heat cometh."
|
||||||
|
status: { la: verified, en: verified }
|
||||||
|
- n: 8
|
||||||
|
text:
|
||||||
|
la: "Et erit fólium ejus víride, et in témpore siccitátis non erit sollícitum, * nec aliquándo désinet fácere fructum."
|
||||||
|
en: "And the leaf thereof shall be green, and in the time of drought it shall not be solicitous, * neither shall it cease at any time to bring forth fruit."
|
||||||
|
status: { la: verified, en: verified }
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Sirach (Ecclesiasticus) 14:22, transcribed from a live Divinum Officium
|
||||||
|
# query (Monastic Tridentinum 1617, command=prayMatutinum, 2026-08-17 —
|
||||||
|
# St. Lawrence's own octave closing day, Matins Nocturn III's first
|
||||||
|
# canticle, "Eccli 14:22;15:3-4;15:6" — see
|
||||||
|
# data/hours/matins-psalmody-overrides/st-lawrence.yml). Only this verse
|
||||||
|
# is authored; the rest of the chapter is unauthored.
|
||||||
|
book: sir
|
||||||
|
chapter: 14
|
||||||
|
verses:
|
||||||
|
- n: 22
|
||||||
|
text:
|
||||||
|
la: "Beátus vir qui in sapiéntia morábitur, et qui in justítia sua meditábitur, * et in sensu cogitábit circumspectiónem Dei."
|
||||||
|
en: "Blessed is the man that shall continue in wisdom, and that shall meditate in his justice, * and in his mind shall think of the all-seeing eye of God."
|
||||||
|
status: { la: verified, en: verified }
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Sirach (Ecclesiasticus) 15:3-4,6, transcribed from a live Divinum
|
||||||
|
# Officium query (Monastic Tridentinum 1617, command=prayMatutinum,
|
||||||
|
# 2026-08-17 — St. Lawrence's own octave closing day, Matins Nocturn
|
||||||
|
# III's first canticle, "Eccli 14:22;15:3-4;15:6" — see
|
||||||
|
# data/hours/matins-psalmody-overrides/st-lawrence.yml). Verse 5 is
|
||||||
|
# skipped by the citation itself, not an authoring gap; only 3-4 and 6
|
||||||
|
# are authored, the rest of the chapter is unauthored.
|
||||||
|
book: sir
|
||||||
|
chapter: 15
|
||||||
|
verses:
|
||||||
|
- n: 3
|
||||||
|
text:
|
||||||
|
la: "Cibábit illum pane vitæ et intelléctus, * et aqua sapiéntiæ salutáris potábit illum:"
|
||||||
|
en: "With the bread of life and understanding, * she shall feed him, and give him the water of wholesome wisdom to drink."
|
||||||
|
status: { la: verified, en: verified }
|
||||||
|
- n: 4
|
||||||
|
text:
|
||||||
|
la: "Et firmábitur in illo, et non flectétur: et continébit illum, et non confundétur: * et exaltábit illum apud próximos suos."
|
||||||
|
en: "And she shall be made strong in him, and he shall not be moved: And she shall hold him fast, and he shall not be confounded: * and she shall exalt him among his neighbours."
|
||||||
|
status: { la: verified, en: verified }
|
||||||
|
- n: 6
|
||||||
|
text:
|
||||||
|
la: "Et nómine ætérno hæreditábit illum * Dóminus Deus noster."
|
||||||
|
en: "And shall cause him to inherit an everlasting name * the Lord our God."
|
||||||
|
status: { la: verified, en: verified }
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Sirach (Ecclesiasticus) 31:8-11, transcribed from a live Divinum
|
||||||
|
# Officium query (Monastic Tridentinum 1617, command=prayMatutinum,
|
||||||
|
# 2026-08-17 — St. Lawrence's own octave closing day, Matins Nocturn
|
||||||
|
# III's third canticle). Verse 10 is split into two half-verses by the
|
||||||
|
# source itself, transcribed as two separate entries under the same
|
||||||
|
# verse number, matching how the source renders it. Only 8-11 are
|
||||||
|
# authored; the rest of the chapter is unauthored.
|
||||||
|
book: sir
|
||||||
|
chapter: 31
|
||||||
|
verses:
|
||||||
|
- n: 8
|
||||||
|
text:
|
||||||
|
la: "Beátus vir, qui invéntus est sine mácula, et qui post aurum non ábiit, * nec sperávit in pecúnia et thesáuris."
|
||||||
|
en: "Blessed is the man that is found without blemish: and that hath not gone after gold, * nor put his trust in money nor in treasures."
|
||||||
|
status: { la: verified, en: verified }
|
||||||
|
- n: 9
|
||||||
|
text:
|
||||||
|
la: "Quis est hic, et laudábimus eum? * fecit enim mirabília in vita sua."
|
||||||
|
en: "Who is he, and we will praise him? * for he hath done wonderful things in his life."
|
||||||
|
status: { la: verified, en: verified }
|
||||||
|
- n: 10
|
||||||
|
text:
|
||||||
|
la: "Qui probátus est in illo, et perféctus est, * erit illi glória ætérna:"
|
||||||
|
en: "Who hath been tried thereby, and made perfect, * he shall have glory everlasting."
|
||||||
|
status: { la: verified, en: verified }
|
||||||
|
- n: 10
|
||||||
|
text:
|
||||||
|
la: "Qui pótuit tránsgredi, et non est transgréssus; * fácere mala, et non fecit."
|
||||||
|
en: "He that could have transgressed, and hath not transgressed: * and could do evil things, and hath not done them."
|
||||||
|
status: { la: verified, en: verified }
|
||||||
|
- n: 11
|
||||||
|
text:
|
||||||
|
la: "Ideo stabilíta sunt bona illíus in Dómino, * et eleemósynas illíus enarrábit omnis ecclésia sanctórum."
|
||||||
|
en: "Therefore are his goods established in the Lord, * and all the church of the saints shall declare his alms."
|
||||||
|
status: { la: verified, en: verified }
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/** Matins' own analogue of hours/lauds-psalmody-overrides.ts, but keyed
|
||||||
|
* by Common category rather than per-saint: live-verified (see
|
||||||
|
* data/hours/matins-psalmody-overrides/st-lawrence.yml's own header)
|
||||||
|
* that the psalm *numbers* for Nocturns 1-2 are shared across saints of
|
||||||
|
* the same Common category (a plain-Duplex Martyr and a plain-Duplex
|
||||||
|
* Martyr-Bishop share all but one of the same twelve psalm numbers) —
|
||||||
|
* only the antiphon text varies, proper to the individual saint when
|
||||||
|
* authored, generic Common-of-* text otherwise. Deliberately the
|
||||||
|
* opposite indexing choice from Lauds' own per-feast override (see that
|
||||||
|
* file's own doc comment) — Lauds' 5-antiphon pool is shared across
|
||||||
|
* every Duplex-majus+ feast regardless of category, so per-feast is the
|
||||||
|
* natural key there; Matins' pool varies *by* category, so per-category
|
||||||
|
* is the natural key here.
|
||||||
|
*
|
||||||
|
* `id` is looked up against `hours/resolve-common.ts`'s
|
||||||
|
* `resolveOfficeWinner(day).id` when it's sanctoral, exactly like every
|
||||||
|
* other per-feast/per-category content lookup in this app — but the
|
||||||
|
* `id` values populated so far are individual saint ids (St. Lawrence
|
||||||
|
* proper), not Common-category ids, since only the live-verified proof
|
||||||
|
* has been authored; extending this to a real Common-category key
|
||||||
|
* (`common-of-a-martyr`, etc., matching `SaintRecord.common`) with
|
||||||
|
* per-saint antiphon overrides layered on top is the deferred bulk-
|
||||||
|
* content work (see TODO.md) — not built yet, so a saint without their
|
||||||
|
* own entry here falls back to the plain ferial weekday table
|
||||||
|
* (matins.ts's own fallback), not to some other saint's proper
|
||||||
|
* antiphons. */
|
||||||
|
export interface MatinsPsalmodyGroup {
|
||||||
|
psalms: number[];
|
||||||
|
antiphon: Partial<Record<string, string>>;
|
||||||
|
}
|
||||||
|
export interface ScriptureCanticleRef {
|
||||||
|
book: string;
|
||||||
|
chapter: number;
|
||||||
|
verses?: string;
|
||||||
|
}
|
||||||
|
export interface MatinsPsalmodyNocturn {
|
||||||
|
groups?: MatinsPsalmodyGroup[];
|
||||||
|
canticles?: { refs: ScriptureCanticleRef[] }[];
|
||||||
|
antiphon?: Partial<Record<string, string>>;
|
||||||
|
versicle: { v: Partial<Record<string, string>>; r: Partial<Record<string, string>> };
|
||||||
|
}
|
||||||
|
export interface MatinsPsalmodyOverride {
|
||||||
|
id: string;
|
||||||
|
nocturn1: MatinsPsalmodyNocturn;
|
||||||
|
nocturn2: MatinsPsalmodyNocturn;
|
||||||
|
nocturn3: MatinsPsalmodyNocturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modules = import.meta.glob<{ default: MatinsPsalmodyOverride }>(
|
||||||
|
'../data/hours/matins-psalmody-overrides/*.yml',
|
||||||
|
{ eager: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const overrides = new Map<string, MatinsPsalmodyOverride>();
|
||||||
|
for (const mod of Object.values(modules)) {
|
||||||
|
overrides.set(mod.default.id, mod.default);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMatinsPsalmodyOverride(id: string | undefined): MatinsPsalmodyOverride | undefined {
|
||||||
|
return id ? overrides.get(id) : undefined;
|
||||||
|
}
|
||||||
+54
-4
@@ -69,6 +69,7 @@ import { resolveCommon, getDayCollect, resolveOfficeWinner, verifiedText, splitN
|
|||||||
import { getBiblePlanReadings } from '../propers/bible-plan';
|
import { getBiblePlanReadings } from '../propers/bible-plan';
|
||||||
import { getNocturnReadings, type NocturnReading } from '../propers/nocturn-readings';
|
import { getNocturnReadings, type NocturnReading } from '../propers/nocturn-readings';
|
||||||
import { getOctaveReading } from '../propers/octave-readings';
|
import { getOctaveReading } from '../propers/octave-readings';
|
||||||
|
import { getMatinsPsalmodyOverride } from './matins-psalmody-overrides';
|
||||||
import matinsSundayAntiphonsData from '../data/hours/matins-sunday-antiphons.yml';
|
import matinsSundayAntiphonsData from '../data/hours/matins-sunday-antiphons.yml';
|
||||||
|
|
||||||
type BilingualText = Partial<Record<string, string>>;
|
type BilingualText = Partial<Record<string, string>>;
|
||||||
@@ -76,9 +77,20 @@ interface SundayGroup {
|
|||||||
psalms: number[];
|
psalms: number[];
|
||||||
antiphon: BilingualText;
|
antiphon: BilingualText;
|
||||||
}
|
}
|
||||||
|
interface ScriptureRef {
|
||||||
|
book: string;
|
||||||
|
chapter: number;
|
||||||
|
verses?: string;
|
||||||
|
}
|
||||||
interface SundayNocturn {
|
interface SundayNocturn {
|
||||||
groups?: SundayGroup[];
|
groups?: SundayGroup[];
|
||||||
canticles?: { book: string; chapter: number; verses?: string }[];
|
// Usually one ref per canticle; more than one when the source cites a
|
||||||
|
// single canticle across a chapter boundary (e.g. St. Lawrence's own
|
||||||
|
// Nocturn 3, "Eccli 14:22;15:3-4;15:6" — one canticle, two Sirach
|
||||||
|
// chapters) — concatenated in order, not rendered as separate
|
||||||
|
// canticles, matching how the source itself presents it as one entry
|
||||||
|
// under one heading.
|
||||||
|
canticles?: { refs: ScriptureRef[] }[];
|
||||||
antiphon?: BilingualText;
|
antiphon?: BilingualText;
|
||||||
versicle: { v: BilingualText; r: BilingualText };
|
versicle: { v: BilingualText; r: BilingualText };
|
||||||
}
|
}
|
||||||
@@ -144,14 +156,14 @@ function sundayCanticleNocturn(group: SundayNocturn, day: LiturgicalDay): Resolv
|
|||||||
const { incipit, full } = splitNamedAntiphon(verifiedText(group.antiphon ?? {}));
|
const { incipit, full } = splitNamedAntiphon(verifiedText(group.antiphon ?? {}));
|
||||||
const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
const opening = isDoubleOrHigher(resolveOfficeWinner(day)) ? full : incipit;
|
||||||
const parts: ResolvedPart[] = (group.canticles ?? []).map((c, i) => {
|
const parts: ResolvedPart[] = (group.canticles ?? []).map((c, i) => {
|
||||||
const verses = getScriptureVerses(c.book, c.chapter, c.verses);
|
const verses = c.refs.flatMap((ref) => getScriptureVerses(ref.book, ref.chapter, ref.verses));
|
||||||
const text: BilingualText = {
|
const text: BilingualText = {
|
||||||
la: verses.map((v) => v.text.la).filter(Boolean).join(' '),
|
la: verses.map((v) => v.text.la).filter(Boolean).join(' '),
|
||||||
en: verses.map((v) => v.text.en).filter(Boolean).join(' '),
|
en: verses.map((v) => v.text.en).filter(Boolean).join(' '),
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
kind: 'canticle' as const,
|
kind: 'canticle' as const,
|
||||||
canticleId: `${c.book}-${c.chapter}${c.verses ? `-${c.verses}` : ''}`,
|
canticleId: c.refs.map((ref) => `${ref.book}-${ref.chapter}${ref.verses ? `-${ref.verses}` : ''}`).join('_'),
|
||||||
text: verifiedText(text),
|
text: verifiedText(text),
|
||||||
antiphon: i === 0 ? opening : undefined,
|
antiphon: i === 0 ? opening : undefined,
|
||||||
};
|
};
|
||||||
@@ -165,6 +177,20 @@ function ferialPsalmody(day: LiturgicalDay): ResolvedPart[] {
|
|||||||
return psalmRefParts(getPsalmsFor('matins', day.weekday));
|
return psalmRefParts(getPsalmsFor('matins', day.weekday));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fallback for a Duplex+ weekday feast with no matins-psalmody-overrides
|
||||||
|
* entry authored for its winner yet (most of them, until the bulk-content
|
||||||
|
* pass — see hours/matins-psalmody-overrides.ts's own doc comment): the
|
||||||
|
* plain ferial weekday table, chunked into 3 nocturns instead of 1 (no
|
||||||
|
* antiphons, no canticles — that content doesn't exist for the ferial
|
||||||
|
* table at all, unlike a real per-category override). Direct instruction:
|
||||||
|
* never blank, but honestly not that saint's own real proper psalmody
|
||||||
|
* until it's authored. */
|
||||||
|
function ferialPsalmodyThreeNocturns(day: LiturgicalDay): [ResolvedPart[], ResolvedPart[], ResolvedPart[]] {
|
||||||
|
const psalms = ferialPsalmody(day);
|
||||||
|
const size = Math.ceil(psalms.length / 3);
|
||||||
|
return [psalms.slice(0, size), psalms.slice(size, size * 2), psalms.slice(size * 2)];
|
||||||
|
}
|
||||||
|
|
||||||
/** Every id whose own patristic/hagiographic/Gospel content should be
|
/** Every id whose own patristic/hagiographic/Gospel content should be
|
||||||
* gathered for `day` — the office winner (if sanctoral), every
|
* gathered for `day` — the office winner (if sanctoral), every
|
||||||
* commemorated saint (a transferred-in feast already appears as `day.winner`
|
* commemorated saint (a transferred-in feast already appears as `day.winner`
|
||||||
@@ -279,7 +305,7 @@ export function resolveOrdo(date: string): ResolvedOrdo {
|
|||||||
{ kind: 'hymn', text: resolveCommon('matins-hymn-ferial') },
|
{ kind: 'hymn', text: resolveCommon('matins-hymn-ferial') },
|
||||||
];
|
];
|
||||||
|
|
||||||
if (threeNocturns) {
|
if (threeNocturns && day.weekday === 'sunday') {
|
||||||
parts.push(...sundayPsalmNocturn(sundayAntiphons.nocturn1, day));
|
parts.push(...sundayPsalmNocturn(sundayAntiphons.nocturn1, day));
|
||||||
parts.push(...(nocturn1Readings ?? []));
|
parts.push(...(nocturn1Readings ?? []));
|
||||||
parts.push(...sundayPsalmNocturn(sundayAntiphons.nocturn2, day));
|
parts.push(...sundayPsalmNocturn(sundayAntiphons.nocturn2, day));
|
||||||
@@ -287,6 +313,30 @@ export function resolveOrdo(date: string): ResolvedOrdo {
|
|||||||
parts.push(...sundayCanticleNocturn(sundayAntiphons.nocturn3, day));
|
parts.push(...sundayCanticleNocturn(sundayAntiphons.nocturn3, day));
|
||||||
parts.push(...(nocturn3Readings ?? []));
|
parts.push(...(nocturn3Readings ?? []));
|
||||||
parts.push({ kind: 'te-deum', text: resolveCommon('te-deum') });
|
parts.push({ kind: 'te-deum', text: resolveCommon('te-deum') });
|
||||||
|
} else if (threeNocturns) {
|
||||||
|
// A Duplex+ weekday feast — genuinely different psalmody from a real
|
||||||
|
// Sunday's (see hours/matins-psalmody-overrides.ts's own doc
|
||||||
|
// comment), not the same content reused. Uses that saint's own
|
||||||
|
// Common-category override when authored, falling back to the plain
|
||||||
|
// ferial weekday table (redistributed into 3 nocturns) otherwise.
|
||||||
|
const override = winner.kind === 'sanctoral' ? getMatinsPsalmodyOverride(winner.id) : undefined;
|
||||||
|
if (override) {
|
||||||
|
parts.push(...sundayPsalmNocturn(override.nocturn1, day));
|
||||||
|
parts.push(...(nocturn1Readings ?? []));
|
||||||
|
parts.push(...sundayPsalmNocturn(override.nocturn2, day));
|
||||||
|
parts.push(...(nocturn2Readings ?? []));
|
||||||
|
parts.push(...sundayCanticleNocturn(override.nocturn3, day));
|
||||||
|
parts.push(...(nocturn3Readings ?? []));
|
||||||
|
} else {
|
||||||
|
const [n1, n2, n3] = ferialPsalmodyThreeNocturns(day);
|
||||||
|
parts.push(...n1);
|
||||||
|
parts.push(...(nocturn1Readings ?? []));
|
||||||
|
parts.push(...n2);
|
||||||
|
parts.push(...(nocturn2Readings ?? []));
|
||||||
|
parts.push(...n3);
|
||||||
|
parts.push(...(nocturn3Readings ?? []));
|
||||||
|
}
|
||||||
|
parts.push({ kind: 'te-deum', text: resolveCommon('te-deum') });
|
||||||
} else {
|
} else {
|
||||||
parts.push(...ferialPsalmody(day));
|
parts.push(...ferialPsalmody(day));
|
||||||
parts.push(...(nocturn1Readings ?? []));
|
parts.push(...(nocturn1Readings ?? []));
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { resolveOrdo } from '../../src/hours';
|
import { resolveOrdo } from '../../src/hours';
|
||||||
|
import { getPsalmsFor } from '../../src/psalter/distribution';
|
||||||
|
|
||||||
// Two clean proof dates, one per branch — see hours/matins.ts's own header
|
// Two clean proof dates, one per branch — see hours/matins.ts's own header
|
||||||
// and TODO.md for why this is a mechanism build with a small content slice,
|
// and TODO.md for why this is a mechanism build with a small content slice,
|
||||||
@@ -120,3 +121,65 @@ describe('resolveOrdo("matins", ...) Sunday (3-nocturn) branch', () => {
|
|||||||
expect(prayerIndex).toBeGreaterThan(teDeumIndex);
|
expect(prayerIndex).toBeGreaterThan(teDeumIndex);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 engine (Monastic Tridentinum 1617, command=prayMatutinum).
|
||||||
|
// The mechanism bug this proves fixed: a Duplex+ weekday feast used to
|
||||||
|
// fall through to reusing the literal *Sunday* psalmody (Ps 20-31)
|
||||||
|
// unconditionally -- confirmed wrong, since a real Duplex+ weekday
|
||||||
|
// feast has its own genuinely different psalmody (see
|
||||||
|
// hours/matins-psalmody-overrides.ts and its own st-lawrence.yml proof
|
||||||
|
// data for the full sourcing detail).
|
||||||
|
const ordo = resolveOrdo('matins', '2026-08-17');
|
||||||
|
|
||||||
|
it("uses St. Lawrence's own proper psalmody, not the Sunday scheme, across 3 nocturns plus a Te Deum", () => {
|
||||||
|
const psalms = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
||||||
|
// Ps 3 + 94 (opening/invitatory), then his own Nocturn 1 (1,2,4,5,8,10)
|
||||||
|
// and Nocturn 2 (14,16,20,23,63,91) -- confirmed live, not the Sunday
|
||||||
|
// scheme's Ps 20-31 at all.
|
||||||
|
expect(psalms).toEqual([3, 94, 1, 2, 4, 5, 8, 10, 14, 16, 20, 23, 63, 91]);
|
||||||
|
expect(ordo.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("every Nocturn 1/2 psalm carries St. Lawrence's own proper antiphon, verified in both languages", () => {
|
||||||
|
const psalmParts = ordo.parts.filter((p) => p.kind === 'psalm') as { psalmNumber: number; antiphon?: { status: Record<string, string> } }[];
|
||||||
|
// Skip Ps 3 (opening, no antiphon) and Ps 94 (the Invitatory, its own
|
||||||
|
// separate antiphon convention) -- the remaining 12 are his own.
|
||||||
|
const nocturnPsalms = psalmParts.filter((p) => ![3, 94].includes(p.psalmNumber));
|
||||||
|
expect(nocturnPsalms).toHaveLength(12);
|
||||||
|
for (const p of nocturnPsalms) {
|
||||||
|
expect(p.antiphon?.status.la).toBe('verified');
|
||||||
|
expect(p.antiphon?.status.en).toBe('verified');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has 3 real OT canticles in Nocturn 3, verified in both languages, sourced from the newly-authored Sirach/Jeremiah chapters', () => {
|
||||||
|
const canticles = ordo.parts.filter((p) => p.kind === 'canticle') as { canticleId: string; text: { status: Record<string, string> } }[];
|
||||||
|
expect(canticles).toHaveLength(3);
|
||||||
|
// The first canticle spans two Sirach chapters under one heading in
|
||||||
|
// the source -- kept as one canticle with two refs, not split.
|
||||||
|
expect(canticles[0]?.canticleId).toBe('sir-14-22_sir-15-3-4_sir-15-6');
|
||||||
|
for (const c of canticles) {
|
||||||
|
expect(c.text.status.la).toBe('verified');
|
||||||
|
expect(c.text.status.en).toBe('verified');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the plain ferial weekday table, redistributed into 3 nocturns, for a Duplex+ weekday winner with no override authored", () => {
|
||||||
|
// St. Ignatius of Antioch (Duplex, Common of a Martyr-Bishop, no
|
||||||
|
// matins-psalmody-overrides entry authored) -- Feb 1 2029, confirmed
|
||||||
|
// clean (no Sunday collision) per his own saint-file comment.
|
||||||
|
const fallback = resolveOrdo('matins', '2029-02-01');
|
||||||
|
const psalms = fallback.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
||||||
|
// Ps 3 + 94, then Thursday's own 9-psalm ferial table (psalter-
|
||||||
|
// distribution.yml), not St. Lawrence's proper psalmody and not the
|
||||||
|
// Sunday scheme either.
|
||||||
|
expect(psalms[0]).toBe(3);
|
||||||
|
expect(psalms[1]).toBe(94);
|
||||||
|
expect(psalms.slice(2)).toEqual(getPsalmsFor('matins', 'thursday').map((r) => r.number));
|
||||||
|
expect(fallback.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
||||||
|
expect(fallback.parts.some((p) => p.kind === 'canticle')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user