Compare commits
2 Commits
8b1c689269
...
63463c696f
| Author | SHA1 | Date | |
|---|---|---|---|
| 63463c696f | |||
| bc432222d1 |
@@ -0,0 +1,16 @@
|
|||||||
|
# Same Common-of-a-Confessor None antiphon as
|
||||||
|
# none-antiphon-common-of-an-abbot.yml — see that file's header comment
|
||||||
|
# for full sourcing/verification detail (live-verified identical across
|
||||||
|
# common-of-an-abbot, this common-of-a-confessor-not-bishop id, and the
|
||||||
|
# bare common-of-a-confessor id — an Abbot is himself a Confessor, the
|
||||||
|
# "faithful servant" antiphon applies generically). Duplicated under this
|
||||||
|
# id rather than aliased, matching the existing convention for this
|
||||||
|
# Common category elsewhere (e.g. matins-hymn-common-of-a-confessor.yml
|
||||||
|
# vs. matins-hymn-common-of-a-confessor-not-bishop.yml).
|
||||||
|
id: none-antiphon-common-of-a-confessor-not-bishop
|
||||||
|
text:
|
||||||
|
la: "Serve bone * et fidélis, intra in gáudium Dómini tui."
|
||||||
|
en: "Thou good and faithful servant, * enter thou into the joy of thy Lord."
|
||||||
|
status:
|
||||||
|
la: verified
|
||||||
|
en: verified
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Same Common-of-a-Confessor None antiphon as
|
||||||
|
# none-antiphon-common-of-an-abbot.yml — see that file's header comment
|
||||||
|
# for full sourcing/verification detail (live-verified identical across
|
||||||
|
# common-of-an-abbot, common-of-a-confessor-not-bishop, and this bare
|
||||||
|
# common-of-a-confessor id — an Abbot is himself a Confessor, the
|
||||||
|
# "faithful servant" antiphon applies generically). Duplicated under this
|
||||||
|
# id rather than aliased, matching the existing convention for this
|
||||||
|
# Common category elsewhere (e.g. matins-hymn-common-of-a-confessor.yml
|
||||||
|
# vs. matins-hymn-common-of-a-confessor-not-bishop.yml).
|
||||||
|
id: none-antiphon-common-of-a-confessor
|
||||||
|
text:
|
||||||
|
la: "Serve bone * et fidélis, intra in gáudium Dómini tui."
|
||||||
|
en: "Thou good and faithful servant, * enter thou into the joy of thy Lord."
|
||||||
|
status:
|
||||||
|
la: verified
|
||||||
|
en: verified
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Same Common-of-a-Confessor Prime antiphon as
|
||||||
|
# prime-antiphon-common-of-an-abbot.yml — see that file's header comment
|
||||||
|
# for full sourcing/verification detail (live-verified identical across
|
||||||
|
# common-of-an-abbot, this common-of-a-confessor-not-bishop id, and the
|
||||||
|
# bare common-of-a-confessor id — an Abbot is himself a Confessor, the
|
||||||
|
# "five talents" antiphon applies generically). Duplicated under this id
|
||||||
|
# rather than aliased, matching the existing convention for this Common
|
||||||
|
# category elsewhere (e.g. matins-hymn-common-of-a-confessor.yml vs.
|
||||||
|
# matins-hymn-common-of-a-confessor-not-bishop.yml).
|
||||||
|
id: prime-antiphon-common-of-a-confessor-not-bishop
|
||||||
|
text:
|
||||||
|
la: "Dómine, quinque talénta * tradidísti mihi, ecce ália quinque superlucrátus sum."
|
||||||
|
en: "Lord, Thou deliverest unto me five talents * behold, I have gained beside them five talents more."
|
||||||
|
status:
|
||||||
|
la: verified
|
||||||
|
en: verified
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Same Common-of-a-Confessor Prime antiphon as
|
||||||
|
# prime-antiphon-common-of-an-abbot.yml — see that file's header comment
|
||||||
|
# for full sourcing/verification detail (live-verified identical across
|
||||||
|
# common-of-an-abbot, common-of-a-confessor-not-bishop, and this bare
|
||||||
|
# common-of-a-confessor id — an Abbot is himself a Confessor, the "five
|
||||||
|
# talents" antiphon applies generically). Duplicated under this id rather
|
||||||
|
# than aliased, matching the existing convention for this Common category
|
||||||
|
# elsewhere (e.g. matins-hymn-common-of-a-confessor.yml vs.
|
||||||
|
# matins-hymn-common-of-a-confessor-not-bishop.yml).
|
||||||
|
id: prime-antiphon-common-of-a-confessor
|
||||||
|
text:
|
||||||
|
la: "Dómine, quinque talénta * tradidísti mihi, ecce ália quinque superlucrátus sum."
|
||||||
|
en: "Lord, Thou deliverest unto me five talents * behold, I have gained beside them five talents more."
|
||||||
|
status:
|
||||||
|
la: verified
|
||||||
|
en: verified
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Same Common-of-a-Confessor Sext antiphon as
|
||||||
|
# sext-antiphon-common-of-an-abbot.yml — see that file's header comment
|
||||||
|
# for full sourcing/verification detail (live-verified identical across
|
||||||
|
# common-of-an-abbot, this common-of-a-confessor-not-bishop id, and the
|
||||||
|
# bare common-of-a-confessor id — an Abbot is himself a Confessor, the
|
||||||
|
# "faithful servant" antiphon applies generically). Duplicated under this
|
||||||
|
# id rather than aliased, matching the existing convention for this
|
||||||
|
# Common category elsewhere (e.g. matins-hymn-common-of-a-confessor.yml
|
||||||
|
# vs. matins-hymn-common-of-a-confessor-not-bishop.yml).
|
||||||
|
id: sext-antiphon-common-of-a-confessor-not-bishop
|
||||||
|
text:
|
||||||
|
la: "Fidélis servus * et prudens, quem constítuit Dóminus super famíliam suam."
|
||||||
|
en: "A faithful and wise servant * whom his Lord hath made ruler over His household."
|
||||||
|
status:
|
||||||
|
la: verified
|
||||||
|
en: verified
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Same Common-of-a-Confessor Sext antiphon as
|
||||||
|
# sext-antiphon-common-of-an-abbot.yml — see that file's header comment
|
||||||
|
# for full sourcing/verification detail (live-verified identical across
|
||||||
|
# common-of-an-abbot, common-of-a-confessor-not-bishop, and this bare
|
||||||
|
# common-of-a-confessor id — an Abbot is himself a Confessor, the
|
||||||
|
# "faithful servant" antiphon applies generically). Duplicated under this
|
||||||
|
# id rather than aliased, matching the existing convention for this
|
||||||
|
# Common category elsewhere (e.g. matins-hymn-common-of-a-confessor.yml
|
||||||
|
# vs. matins-hymn-common-of-a-confessor-not-bishop.yml).
|
||||||
|
id: sext-antiphon-common-of-a-confessor
|
||||||
|
text:
|
||||||
|
la: "Fidélis servus * et prudens, quem constítuit Dóminus super famíliam suam."
|
||||||
|
en: "A faithful and wise servant * whom his Lord hath made ruler over His household."
|
||||||
|
status:
|
||||||
|
la: verified
|
||||||
|
en: verified
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Same Common-of-a-Confessor Terce antiphon as
|
||||||
|
# terce-antiphon-common-of-an-abbot.yml — see that file's header comment
|
||||||
|
# for full sourcing/verification detail (live-verified identical across
|
||||||
|
# common-of-an-abbot, this common-of-a-confessor-not-bishop id, and the
|
||||||
|
# bare common-of-a-confessor id — an Abbot is himself a Confessor, the
|
||||||
|
# "faithful servant" antiphon applies generically). Duplicated under this
|
||||||
|
# id rather than aliased, matching the existing convention for this
|
||||||
|
# Common category elsewhere (e.g. matins-hymn-common-of-a-confessor.yml
|
||||||
|
# vs. matins-hymn-common-of-a-confessor-not-bishop.yml).
|
||||||
|
id: terce-antiphon-common-of-a-confessor-not-bishop
|
||||||
|
text:
|
||||||
|
la: "Euge, serve bone * in módico fidélis, intra in gáudium Dómini tui."
|
||||||
|
en: "Well done, thou good servant, * thou hast been faithful in a very little, enter thou into the joy of thy Lord."
|
||||||
|
status:
|
||||||
|
la: verified
|
||||||
|
en: verified
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Same Common-of-a-Confessor Terce antiphon as
|
||||||
|
# terce-antiphon-common-of-an-abbot.yml — see that file's header comment
|
||||||
|
# for full sourcing/verification detail (live-verified identical across
|
||||||
|
# common-of-an-abbot, common-of-a-confessor-not-bishop, and this bare
|
||||||
|
# common-of-a-confessor id — an Abbot is himself a Confessor, the
|
||||||
|
# "faithful servant" antiphon applies generically). Duplicated under this
|
||||||
|
# id rather than aliased, matching the existing convention for this
|
||||||
|
# Common category elsewhere (e.g. matins-hymn-common-of-a-confessor.yml
|
||||||
|
# vs. matins-hymn-common-of-a-confessor-not-bishop.yml).
|
||||||
|
id: terce-antiphon-common-of-a-confessor
|
||||||
|
text:
|
||||||
|
la: "Euge, serve bone * in módico fidélis, intra in gáudium Dómini tui."
|
||||||
|
en: "Well done, thou good servant, * thou hast been faithful in a very little, enter thou into the joy of thy Lord."
|
||||||
|
status:
|
||||||
|
la: verified
|
||||||
|
en: verified
|
||||||
@@ -93,5 +93,24 @@ for (const mod of Object.values(categoryModules)) {
|
|||||||
|
|
||||||
export function getMatinsPsalmodyOverride(id: string | undefined): MatinsPsalmodyScheme | undefined {
|
export function getMatinsPsalmodyOverride(id: string | undefined): MatinsPsalmodyScheme | undefined {
|
||||||
if (!id) return undefined;
|
if (!id) return undefined;
|
||||||
return overrides.get(id) ?? categories.get(getSaintRecord(id)?.common ?? '');
|
return getMatinsSaintOverride(id) ?? getMatinsCommonOverride(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Saint-specific proper only, skipping the Common-category tier — used
|
||||||
|
* where the caller needs to know specifically whether a *proper* exists,
|
||||||
|
* separately from a Common fallback (e.g. matins.ts's duplex-rank case,
|
||||||
|
* which treats "proper exists" and "only Common exists" differently). */
|
||||||
|
export function getMatinsSaintOverride(id: string | undefined): MatinsPsalmodyOverride | undefined {
|
||||||
|
if (!id) return undefined;
|
||||||
|
return overrides.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Common-category lookup only, skipping the saint-specific `overrides`
|
||||||
|
* tier — used where the caller has already handled (or ruled out) a
|
||||||
|
* saint-specific proper separately, e.g. matins.ts's duplex-rank fallback,
|
||||||
|
* which only wants Common's per-nocturn versicle/Nocturn-3 antiphon, not
|
||||||
|
* a full scheme substitution. */
|
||||||
|
export function getMatinsCommonOverride(id: string | undefined): MatinsPsalmodyScheme | undefined {
|
||||||
|
if (!id) return undefined;
|
||||||
|
return categories.get(getSaintRecord(id)?.common ?? '');
|
||||||
}
|
}
|
||||||
|
|||||||
+109
-30
@@ -61,8 +61,8 @@
|
|||||||
// ferial day, one clean Sunday) — this is the mechanism build, not the
|
// ferial day, one clean Sunday) — this is the mechanism build, not the
|
||||||
// full-calendar content pass. See TODO.md for what's deferred.
|
// full-calendar content pass. See TODO.md for what's deferred.
|
||||||
import type { ResolvedOrdo, ResolvedPart, ResolvedText, ResolvedVerse } from './types';
|
import type { ResolvedOrdo, ResolvedPart, ResolvedText, ResolvedVerse } from './types';
|
||||||
import type { LiturgicalDay } from '../calendar/types';
|
import type { LiturgicalDay, DayWinner } from '../calendar/types';
|
||||||
import { resolveDay, resolveTemporalId, monthWeekId, activeOctavesFor, resolveActiveOctave } from '../calendar';
|
import { resolveDay, resolveTemporalId, monthWeekId, activeOctavesFor, resolveActiveOctave, isAtLeast } from '../calendar';
|
||||||
import { isInTriduum } from '../calendar/temporal';
|
import { isInTriduum } from '../calendar/temporal';
|
||||||
import { getDayLabel } from '../calendar/day-label';
|
import { getDayLabel } from '../calendar/day-label';
|
||||||
import { getPsalmVerses } from '../psalter';
|
import { getPsalmVerses } from '../psalter';
|
||||||
@@ -84,7 +84,7 @@ import { getSaintRecord } from '../calendar/feasts';
|
|||||||
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 { getMatinsSaintOverride, getMatinsCommonOverride, type MatinsPsalmodyScheme } from './matins-psalmody-overrides';
|
||||||
import matinsSundayAntiphonsData from '../data/hours/matins-sunday-antiphons.yml';
|
import matinsSundayAntiphonsData from '../data/hours/matins-sunday-antiphons.yml';
|
||||||
import matinsFerialAntiphonsData from '../data/hours/matins-ferial-antiphons.yml';
|
import matinsFerialAntiphonsData from '../data/hours/matins-ferial-antiphons.yml';
|
||||||
import type { Weekday } from '../calendar/types';
|
import type { Weekday } from '../calendar/types';
|
||||||
@@ -339,10 +339,6 @@ function resolveMatinsHymn(day: LiturgicalDay): ResolvedText {
|
|||||||
return resolveCommon('matins-hymn-ferial');
|
return resolveCommon('matins-hymn-ferial');
|
||||||
}
|
}
|
||||||
|
|
||||||
function ferialPsalmody(day: LiturgicalDay): ResolvedPart[] {
|
|
||||||
return psalmRefParts(getPsalmsFor('matins', day.weekday));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The plain ferial weekday nocturn's real antiphoned psalmody (see
|
/** The plain ferial weekday nocturn's real antiphoned psalmody (see
|
||||||
* data/hours/matins-ferial-antiphons.yml's own header for how each
|
* data/hours/matins-ferial-antiphons.yml's own header for how each
|
||||||
* weekday's groups/versicle were reconciled against psalter-
|
* weekday's groups/versicle were reconciled against psalter-
|
||||||
@@ -378,17 +374,81 @@ function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Fallback for a Duplex+ weekday feast with no matins-psalmody-overrides
|
/** Fallback for a Duplex+ weekday feast with no matins-psalmody-overrides
|
||||||
* entry authored for its winner yet (most of them, until the bulk-content
|
* entry authored for its winner yet: the plain ferial weekday table,
|
||||||
* pass — see hours/matins-psalmody-overrides.ts's own doc comment): the
|
* chunked into 3 nocturns instead of 1. Bare (no antiphons, no canticles)
|
||||||
* plain ferial weekday table, chunked into 3 nocturns instead of 1 (no
|
* when `common` isn't given — genuinely nothing authored at all for this
|
||||||
* antiphons, no canticles — that content doesn't exist for the ferial
|
* winner, still used as-is for a duplex-majus+ winner with no Common
|
||||||
* table at all, unlike a real per-category override). Direct instruction:
|
* category either.
|
||||||
* never blank, but honestly not that saint's own real proper psalmody
|
*
|
||||||
* until it's authored. */
|
* When `common`/`commonId` *are* given (a duplex-rank winner whose Common
|
||||||
function ferialPsalmodyThreeNocturns(day: LiturgicalDay): [ResolvedPart[], ResolvedPart[], ResolvedPart[]] {
|
* category exists — user, 2026-08), each nocturn borrows that category's
|
||||||
const psalms = ferialPsalmody(day);
|
* own `nocturn{N}.versicle`: the ferial table only ever authors one
|
||||||
const size = Math.ceil(psalms.length / 3);
|
* versicle for the whole (normally 1-nocturn) day, so a 3-nocturn split
|
||||||
return [psalms.slice(0, size), psalms.slice(size, size * 2), psalms.slice(size * 2)];
|
* leaves 2 of the 3 nocturns with none at all — Common's own 3 real
|
||||||
|
* versicles are an exact fit for that gap.
|
||||||
|
*
|
||||||
|
* For the antiphon: Common's own Matins Nocturn 1/2 are 6 individual
|
||||||
|
* per-psalm antiphons each written as a paraphrase of *that* psalm's own
|
||||||
|
* opening words (e.g. Ps 4's "Invocántem exaudívit Dóminus" echoing Ps 4's
|
||||||
|
* own "Cum invocárem exaudívit me") — laid over a *different* ferial psalm
|
||||||
|
* they'd misquote it, so they can't be borrowed. Instead, each nocturn
|
||||||
|
* gets `SaintRecord.minorHoursCommon`'s own Terce/Sext/None antiphon
|
||||||
|
* (`{terce,sext,none}-antiphon-${minorHoursCommon}`) — the same per-saint
|
||||||
|
* field `resolveMinorHourAntiphon` itself uses, deliberately distinct
|
||||||
|
* from `common` (a saint's minor-hour Common category isn't always the
|
||||||
|
* same as their overall one — see `SaintRecord`'s own doc comment).
|
||||||
|
* Real, verified, and already proven non-psalm-specific by the minor hours themselves (3
|
||||||
|
* ferial psalms of arbitrary content under one thematic antiphon,
|
||||||
|
* live-verified against the reference engine at any rank). Terce/Sext/
|
||||||
|
* None each carry their own distinct text per category (confirmed,
|
||||||
|
* 2026-08-27), so this gives Nocturns 1/2/3 three genuinely different
|
||||||
|
* real antiphons instead of one borrowed Nocturn-3-only text — user,
|
||||||
|
* 2026-08-27: "some antiphons from the feria and some from the commons
|
||||||
|
* feels off... use those antiphons, one per nocturn." Falls back to bare
|
||||||
|
* (no antiphon) per nocturn when that particular hour+category combo
|
||||||
|
* isn't authored yet (most minor-hour Common files only cover a handful
|
||||||
|
* of categories so far) — never a mismatched substitute. */
|
||||||
|
function ferialPsalmodyThreeNocturns(
|
||||||
|
day: LiturgicalDay,
|
||||||
|
common?: MatinsPsalmodyScheme,
|
||||||
|
commonId?: string,
|
||||||
|
): [ResolvedPart[], ResolvedPart[], ResolvedPart[]] {
|
||||||
|
const refs = getPsalmsFor('matins', day.weekday);
|
||||||
|
const size = Math.ceil(refs.length / 3);
|
||||||
|
const refChunks: [PsalmRef[], PsalmRef[], PsalmRef[]] = [
|
||||||
|
refs.slice(0, size),
|
||||||
|
refs.slice(size, size * 2),
|
||||||
|
refs.slice(size * 2),
|
||||||
|
];
|
||||||
|
if (!common) {
|
||||||
|
return refChunks.map((chunk) => psalmRefParts(chunk)) as [ResolvedPart[], ResolvedPart[], ResolvedPart[]];
|
||||||
|
}
|
||||||
|
const winner = resolveOfficeWinner(day);
|
||||||
|
const nocturnVersicles = [common.nocturn1.versicle, common.nocturn2.versicle, common.nocturn3.versicle];
|
||||||
|
const minorHours = ['terce', 'sext', 'none'] as const;
|
||||||
|
return refChunks.map((chunk, i) => {
|
||||||
|
const antiphonText = commonId ? resolveCommon(`${minorHours[i]}-antiphon-${commonId}`) : undefined;
|
||||||
|
const authored = antiphonText && (antiphonText.status.la !== 'missing' || antiphonText.status.en !== 'missing');
|
||||||
|
const psalms = authored ? withSingleAntiphon(chunk, antiphonText!, winner) : psalmRefParts(chunk);
|
||||||
|
return [...psalms, { kind: 'versicle', text: versicleText(nocturnVersicles[i]!) }];
|
||||||
|
}) as [ResolvedPart[], ResolvedPart[], ResolvedPart[]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wraps a flat list of psalm refs in one opening/closing antiphon — the
|
||||||
|
* shape `sundayCanticleNocturn` already uses for its own single antiphon
|
||||||
|
* over several canticles, reused here for `ferialPsalmodyThreeNocturns`'s
|
||||||
|
* borrowed Terce/Sext/None antiphon over ferial psalms. */
|
||||||
|
function withSingleAntiphon(refs: PsalmRef[], antiphonText: ResolvedText, winner: DayWinner): ResolvedPart[] {
|
||||||
|
const opening = openingAntiphon(antiphonText, winner);
|
||||||
|
const { full } = splitNamedAntiphon(antiphonText);
|
||||||
|
const parts: ResolvedPart[] = [];
|
||||||
|
refs.forEach((ref, i) => {
|
||||||
|
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, i === 0 ? opening : undefined);
|
||||||
|
parts.push({ kind: 'psalm', psalmNumber: ref.number, verses, antiphon: partAntiphon });
|
||||||
|
});
|
||||||
|
parts.push({ kind: 'antiphon', text: full });
|
||||||
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Every id whose own patristic/hagiographic/Gospel content should be
|
/** Every id whose own patristic/hagiographic/Gospel content should be
|
||||||
@@ -578,25 +638,44 @@ export function resolveOrdo(date: string): ResolvedOrdo {
|
|||||||
parts.push({ kind: 'versicle', text: resolveCommon('te-decet-laus') });
|
parts.push({ kind: 'versicle', text: resolveCommon('te-decet-laus') });
|
||||||
} else if (threeNocturns) {
|
} else if (threeNocturns) {
|
||||||
// A Duplex+ weekday feast — genuinely different psalmody from a real
|
// A Duplex+ weekday feast — genuinely different psalmody from a real
|
||||||
// Sunday's, not the same content reused. getMatinsPsalmodyOverride
|
// Sunday's, not the same content reused.
|
||||||
// itself does the proper -> Common -> (nothing) tiering; only the
|
// - Saint-specific proper: use its own full scheme, any rank.
|
||||||
// final "nothing authored at all" tier — the plain ferial weekday
|
// - No proper, rank duplex-majus+: Common's own full scheme (own
|
||||||
// table, redistributed into 3 nocturns — lives here (see
|
// psalm numbers, canticles) if authored; else the fully bare
|
||||||
// hours/matins-psalmody-overrides.ts's own doc comment for the full
|
// ferialPsalmodyThreeNocturns stand-in. Unaffected by the
|
||||||
// three-tier picture).
|
// duplex-and-below carve-out below (user, 2026-08).
|
||||||
const override = winner.kind === 'sanctoral' ? getMatinsPsalmodyOverride(winner.id) : undefined;
|
// - No proper, rank exactly duplex (the floor of 3-nocturn
|
||||||
if (override) {
|
// eligibility): ferial psalm numbers, not Common's own — but
|
||||||
|
// Common's per-nocturn versicle, and each nocturn's own Terce/
|
||||||
|
// Sext/None antiphon for that category, if authored (see
|
||||||
|
// ferialPsalmodyThreeNocturns's own doc comment for why those
|
||||||
|
// three, not Common's own Matins Nocturn antiphons).
|
||||||
|
const properOverride = winner.kind === 'sanctoral' ? getMatinsSaintOverride(winner.id) : undefined;
|
||||||
|
const isAboveDuplex = winner.kind === 'sanctoral' && isAtLeast(winner.rank, 'duplex-majus');
|
||||||
|
const commonOverride = winner.kind === 'sanctoral' && !properOverride ? getMatinsCommonOverride(winner.id) : undefined;
|
||||||
|
if (properOverride) {
|
||||||
parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[0]}` });
|
parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[0]}` });
|
||||||
parts.push(...sundayPsalmNocturn(override.nocturn1, day));
|
parts.push(...sundayPsalmNocturn(properOverride.nocturn1, day));
|
||||||
parts.push(...(nocturn1Readings ?? []));
|
parts.push(...(nocturn1Readings ?? []));
|
||||||
parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[1]}` });
|
parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[1]}` });
|
||||||
parts.push(...sundayPsalmNocturn(override.nocturn2, day));
|
parts.push(...sundayPsalmNocturn(properOverride.nocturn2, day));
|
||||||
parts.push(...(nocturn2Readings ?? []));
|
parts.push(...(nocturn2Readings ?? []));
|
||||||
parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[2]}` });
|
parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[2]}` });
|
||||||
parts.push(...sundayCanticleNocturn(override.nocturn3, day));
|
parts.push(...sundayCanticleNocturn(properOverride.nocturn3, day));
|
||||||
|
parts.push(...(nocturn3Readings ?? []));
|
||||||
|
} else if (isAboveDuplex && commonOverride) {
|
||||||
|
parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[0]}` });
|
||||||
|
parts.push(...sundayPsalmNocturn(commonOverride.nocturn1, day));
|
||||||
|
parts.push(...(nocturn1Readings ?? []));
|
||||||
|
parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[1]}` });
|
||||||
|
parts.push(...sundayPsalmNocturn(commonOverride.nocturn2, day));
|
||||||
|
parts.push(...(nocturn2Readings ?? []));
|
||||||
|
parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[2]}` });
|
||||||
|
parts.push(...sundayCanticleNocturn(commonOverride.nocturn3, day));
|
||||||
parts.push(...(nocturn3Readings ?? []));
|
parts.push(...(nocturn3Readings ?? []));
|
||||||
} else {
|
} else {
|
||||||
const [n1, n2, n3] = ferialPsalmodyThreeNocturns(day);
|
const commonId = !isAboveDuplex && winner.kind === 'sanctoral' ? getSaintRecord(winner.id)?.minorHoursCommon : undefined;
|
||||||
|
const [n1, n2, n3] = ferialPsalmodyThreeNocturns(day, isAboveDuplex ? undefined : commonOverride, commonId);
|
||||||
parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[0]}` });
|
parts.push({ kind: 'section-heading', label: `Nocturn ${NOCTURN_NUMERAL[0]}` });
|
||||||
parts.push(...n1);
|
parts.push(...n1);
|
||||||
parts.push(...(nocturn1Readings ?? []));
|
parts.push(...(nocturn1Readings ?? []));
|
||||||
|
|||||||
+69
-52
@@ -226,90 +226,107 @@ describe('resolveOrdo("matins", ...) Duplex+ weekday-feast (3-nocturn, non-Sunda
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls through to its Common category's own generic psalmody for a Duplex+ weekday winner with no proper antiphon authored", () => {
|
it("uses ferial psalms with its own Terce/Sext/None antiphons (one per nocturn) plus Common's own versicles, for a plain-Duplex weekday winner with no proper antiphon authored", () => {
|
||||||
// St. Ignatius of Antioch (Duplex, Common of a Martyr-Bishop, no
|
// St. Ignatius of Antioch (Duplex, Common of a Martyr-Bishop, no
|
||||||
// proper antiphon of his own) -- Feb 1 2029, confirmed clean (no
|
// proper antiphon of his own) -- Feb 1 2029, a Thursday, confirmed
|
||||||
// Sunday collision) per his own saint-file comment.
|
// clean (no Sunday collision) per his own saint-file comment.
|
||||||
// getMatinsPsalmodyOverride's Common tier resolves
|
// Duplex is the floor of 3-nocturn eligibility (user, 2026-08): the
|
||||||
// common-of-a-martyr-bishop via common-of-a-martyr.yml's own
|
// psalm numbers are Thursday's own real ferial table
|
||||||
// `aliases` (the reference engine's own Matins psalmody for
|
// (psalter-distribution.yml), chunked into 3 -- not Common's own
|
||||||
// Martyr-Bishop redirects outright to plain Martyr) -- real,
|
// psalm numbers, which only duplex-majus+ winners still get in full.
|
||||||
// verified Common text, not the plain ferial table.
|
// Each nocturn's versicle is Common's own (getMatinsCommonOverride's
|
||||||
|
// Common-of-a-Martyr-Bishop resolves via common-of-a-martyr.yml's
|
||||||
|
// `aliases`); each nocturn's antiphon is his own SaintRecord.
|
||||||
|
// minorHoursCommon (common-of-a-martyr, live-verified identical
|
||||||
|
// across the Bishop/non-Bishop split) own Terce/Sext/None antiphon --
|
||||||
|
// real, verified, non-psalm-specific text, one distinct antiphon per
|
||||||
|
// nocturn (user, 2026-08-27: "some antiphons from the feria and some
|
||||||
|
// from the commons feels off... use those antiphons, one per
|
||||||
|
// nocturn"), not Common's own Matins Nocturn 1/2 antiphons (those are
|
||||||
|
// 6 individual per-psalm paraphrases that would misquote a different
|
||||||
|
// ferial psalm).
|
||||||
const common = resolveOrdo('matins', '2029-02-01');
|
const common = resolveOrdo('matins', '2029-02-01');
|
||||||
const psalms = common.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
const psalms = common.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
||||||
// Same Nocturn 1 as St. Lawrence's own (1,2,4,5,8,10), but Nocturn 2
|
expect(psalms).toEqual([3, 94, 76, 77, 77, 78, 79, 80, 82, 83, 84]);
|
||||||
// uses the Common default's Ps 64, not Lawrence's own proper Ps 16.
|
|
||||||
expect(psalms).toEqual([3, 94, 1, 2, 4, 5, 8, 10, 14, 20, 23, 63, 64, 91]);
|
|
||||||
expect(common.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
expect(common.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
||||||
expect(common.parts.some((p) => p.kind === 'canticle')).toBe(true);
|
// No canticles -- the ferial table has none, and this tier doesn't
|
||||||
|
// borrow Common's own (unlike a duplex-majus+ winner's full scheme).
|
||||||
|
expect(common.parts.some((p) => p.kind === 'canticle')).toBe(false);
|
||||||
|
|
||||||
const psalmParts = common.parts.filter((p) => p.kind === 'psalm') as { psalmNumber: number; antiphon?: { status: Record<string, string> } }[];
|
const psalmParts = common.parts.filter((p) => p.kind === 'psalm') as { psalmNumber: number; antiphon?: { text: Record<string, string>; status: Record<string, string> } }[];
|
||||||
const nocturnPsalms = psalmParts.filter((p) => ![3, 94].includes(p.psalmNumber));
|
const nocturnPsalms = psalmParts.filter((p) => ![3, 94].includes(p.psalmNumber));
|
||||||
// The generic Common antiphon text is real and verified, even though
|
// Nocturn 1 (76,77,77): Terce's own Common-of-a-Martyr antiphon.
|
||||||
// it is not proper to Ignatius himself.
|
expect(nocturnPsalms[0]?.antiphon?.text.la).toBe('Ant. Qui séquitur me, * non ámbulat in ténebris, sed habébit lumen vitæ, dicit Dóminus.');
|
||||||
for (const p of nocturnPsalms) {
|
expect(nocturnPsalms.slice(1, 3).every((p) => p.antiphon === undefined)).toBe(true);
|
||||||
expect(p.antiphon?.status.la).toBe('verified');
|
// Nocturn 2 (78,79,80): Sext's own, a genuinely different antiphon.
|
||||||
expect(p.antiphon?.status.en).toBe('verified');
|
expect(nocturnPsalms[3]?.antiphon?.text.la).toBe('Ant. Qui mihi minístrat, * me sequátur: et ubi ego sum, illic sit et miníster meus.');
|
||||||
|
expect(nocturnPsalms.slice(4, 6).every((p) => p.antiphon === undefined)).toBe(true);
|
||||||
|
// Nocturn 3 (82,83,84): None's own, different again.
|
||||||
|
expect(nocturnPsalms[6]?.antiphon?.text.la).toBe('Ant. Volo Pater, * ut ubi ego sum, illic sit et miníster meus.');
|
||||||
|
expect(nocturnPsalms.slice(7).every((p) => p.antiphon === undefined)).toBe(true);
|
||||||
|
for (const i of [0, 3, 6]) {
|
||||||
|
expect(nocturnPsalms[i]?.antiphon?.status.la).toBe('verified');
|
||||||
|
expect(nocturnPsalms[i]?.antiphon?.status.en).toBe('verified');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls through to Common-of-a-Confessor's own generic psalmody for a Duplex+ weekday winner with no proper antiphon authored", () => {
|
it("uses ferial psalms with Common-of-an-Abbot's own Terce/Sext/None antiphons (St. Francis of Paola's own minorHoursCommon, distinct from his Matins-classification common) for a plain-Duplex weekday winner with no proper antiphon authored", () => {
|
||||||
// St. Francis of Paola (Duplex, Common of a Confessor Not a Bishop,
|
// St. Francis of Paola (Duplex, Common of a Confessor Not a Bishop
|
||||||
// no proper antiphon of his own) -- 2030-04-02, confirmed clean (no
|
// for `common`, but `minorHoursCommon: common-of-an-abbot` -- his
|
||||||
// Sunday collision, safely outside Holy Week/Easter octave that year)
|
// own P/T/S/N draw from the Abbot common instead, live-verified per
|
||||||
// per his own saint-file comment.
|
// his own saint-file comment) -- 2030-04-02, a Tuesday, confirmed
|
||||||
|
// clean (no Sunday collision, safely outside Holy Week/Easter octave
|
||||||
|
// that year).
|
||||||
const common = resolveOrdo('matins', '2030-04-02');
|
const common = resolveOrdo('matins', '2030-04-02');
|
||||||
const psalms = common.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
const psalms = common.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
||||||
expect(psalms).toEqual([3, 94, 1, 2, 4, 5, 8, 10, 14, 20, 23, 95, 96, 97]);
|
expect(psalms).toEqual([3, 94, 43, 44, 47, 48, 49, 54, 55, 58, 59]);
|
||||||
expect(common.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
expect(common.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
||||||
expect(common.parts.some((p) => p.kind === 'canticle')).toBe(true);
|
expect(common.parts.some((p) => p.kind === 'canticle')).toBe(false);
|
||||||
|
|
||||||
const psalmParts = common.parts.filter((p) => p.kind === 'psalm') as { psalmNumber: number; antiphon?: { status: Record<string, string> } }[];
|
const psalmParts = common.parts.filter((p) => p.kind === 'psalm') as { psalmNumber: number; antiphon?: { text: Record<string, string>; status: Record<string, string> } }[];
|
||||||
const nocturnPsalms = psalmParts.filter((p) => ![3, 94].includes(p.psalmNumber));
|
const nocturnPsalms = psalmParts.filter((p) => ![3, 94].includes(p.psalmNumber));
|
||||||
for (const p of nocturnPsalms) {
|
expect(nocturnPsalms[0]?.antiphon?.text.la).toBe('Ant. Euge, serve bone * in módico fidélis, intra in gáudium Dómini tui.');
|
||||||
expect(p.antiphon?.status.la).toBe('verified');
|
expect(nocturnPsalms[3]?.antiphon?.text.la).toBe('Ant. Fidélis servus * et prudens, quem constítuit Dóminus super famíliam suam.');
|
||||||
expect(p.antiphon?.status.en).toBe('verified');
|
expect(nocturnPsalms[6]?.antiphon?.text.la).toBe('Ant. Serve bone * et fidélis, intra in gáudium Dómini tui.');
|
||||||
|
for (const i of [0, 3, 6]) {
|
||||||
|
expect(nocturnPsalms[i]?.antiphon?.status.la).toBe('verified');
|
||||||
|
expect(nocturnPsalms[i]?.antiphon?.status.en).toBe('verified');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls through to Common-of-a-Confessor-Bishop's own generic psalmody -- same psalm numbers as Common of a Confessor, different Nocturn versicles", () => {
|
it("uses ferial psalms for a Common-of-a-Confessor-Bishop winner -- same weekday psalm numbers as any other plain-Duplex Thursday winner, since psalm numbers no longer come from Common at all", () => {
|
||||||
// St. Basil the Great (Duplex, live-verified as byte-identical to
|
// St. Basil the Great (Duplex, live-verified as byte-identical to
|
||||||
// common-of-a-confessor-bishop.yml despite his own [Rule] saying
|
// common-of-a-confessor-bishop.yml despite his own [Rule] saying
|
||||||
// "vide C4a") -- 2035-06-14, confirmed clean per his own saint-file
|
// "vide C4a") -- 2035-06-14, a Thursday, confirmed clean per his own
|
||||||
// comment.
|
// saint-file comment. Same Thursday ferial psalms as St. Ignatius of
|
||||||
|
// Antioch's own test above -- the two Commons only ever differed in
|
||||||
|
// versicles/antiphon, and psalm numbers are ferial now regardless of
|
||||||
|
// which Common category applies.
|
||||||
const common = resolveOrdo('matins', '2035-06-14');
|
const common = resolveOrdo('matins', '2035-06-14');
|
||||||
const psalms = common.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
const psalms = common.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
||||||
// Same psalm numbers in every slot as Common of a Confessor Not a
|
expect(psalms).toEqual([3, 94, 76, 77, 77, 78, 79, 80, 82, 83, 84]);
|
||||||
// Bishop above -- the two Commons only differ in their versicles.
|
|
||||||
expect(psalms).toEqual([3, 94, 1, 2, 4, 5, 8, 10, 14, 20, 23, 95, 96, 97]);
|
|
||||||
expect(common.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
expect(common.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
||||||
expect(common.parts.some((p) => p.kind === 'canticle')).toBe(true);
|
expect(common.parts.some((p) => p.kind === 'canticle')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls through to Common-of-Several-Martyrs's own generic psalmody -- a genuinely different scheme from the single-martyr Common", () => {
|
it("uses ferial psalms with Common-of-Several-Martyrs's own Terce/Sext/None antiphons (one per nocturn) for a plain-Duplex weekday winner with no proper antiphon authored", () => {
|
||||||
// Ss. Fabian and Sebastian (Duplex, Common of Several Martyrs, no
|
// Ss. Fabian and Sebastian (Duplex, Common of Several Martyrs, no
|
||||||
// proper antiphon of their own) -- 2029-01-20, a Saturday, confirmed
|
// proper antiphon of their own) -- 2029-01-20, a Saturday, confirmed
|
||||||
// clean (no Sunday collision) per their own saint-file comment.
|
// clean (no Sunday collision) per their own saint-file comment.
|
||||||
const common = resolveOrdo('matins', '2029-01-20');
|
const common = resolveOrdo('matins', '2029-01-20');
|
||||||
const psalms = common.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
const psalms = common.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
||||||
expect(psalms).toEqual([3, 94, 1, 2, 10, 14, 15, 23, 32, 33, 45, 60, 63, 78]);
|
expect(psalms).toEqual([3, 94, 96, 104, 104, 105, 105, 106, 106, 107, 108]);
|
||||||
expect(common.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
expect(common.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
||||||
expect(common.parts.some((p) => p.kind === 'canticle')).toBe(true);
|
expect(common.parts.some((p) => p.kind === 'canticle')).toBe(false);
|
||||||
|
|
||||||
const psalmParts = common.parts.filter((p) => p.kind === 'psalm') as { psalmNumber: number; antiphon?: { status: Record<string, string> } }[];
|
const psalmParts = common.parts.filter((p) => p.kind === 'psalm') as { psalmNumber: number; antiphon?: { text: Record<string, string>; status: Record<string, string> } }[];
|
||||||
const nocturnPsalms = psalmParts.filter((p) => ![3, 94].includes(p.psalmNumber));
|
const nocturnPsalms = psalmParts.filter((p) => ![3, 94].includes(p.psalmNumber));
|
||||||
for (const p of nocturnPsalms) {
|
expect(nocturnPsalms[0]?.antiphon?.text.la).toBe('Ant. Cum palma * ad regna pervenérunt Sancti, corónas decóris meruérunt de manu Dei.');
|
||||||
expect(p.antiphon?.status.la).toBe('verified');
|
expect(nocturnPsalms[3]?.antiphon?.text.la).toBe('Ant. Córpora Sanctórum * in pace sepúlta sunt: et vivent nómina eórum in ætérnum.');
|
||||||
expect(p.antiphon?.status.en).toBe('verified');
|
expect(nocturnPsalms[6]?.antiphon?.text.la).toBe('Ant. Mártyrum chorus * laudáte Dóminum de cælis, allelúja.');
|
||||||
}
|
expect(nocturnPsalms[6]?.antiphon?.status.la).toBe('verified');
|
||||||
|
expect(nocturnPsalms[6]?.antiphon?.status.en).toBe('verified');
|
||||||
const canticles = common.parts.filter((p) => p.kind === 'canticle') as { canticleId: string; text: { status: Record<string, string> } }[];
|
expect(nocturnPsalms.slice(7).every((p) => p.antiphon === undefined)).toBe(true);
|
||||||
expect(canticles).toHaveLength(3);
|
|
||||||
expect(canticles[0]?.canticleId).toBe('sap-3-1-6');
|
|
||||||
for (const c of canticles) {
|
|
||||||
expect(c.text.status.la).toBe('verified');
|
|
||||||
expect(c.text.status.en).toBe('verified');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls all the way back to the plain ferial weekday table, redistributed into 3 nocturns, when neither a proper nor a Common entry is authored', () => {
|
it('falls all the way back to the plain ferial weekday table, redistributed into 3 nocturns, when neither a proper nor a Common entry is authored', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user