Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f4d4eb3738 | |||
| 9da5a74301 |
@@ -4838,3 +4838,41 @@ category of move as this app's existing late-canonization rank-deflation convent
|
|||||||
the other direction — bringing his own count to 6.
|
the other direction — bringing his own count to 6.
|
||||||
|
|
||||||
`npm test` (821 passed), `tsc --noEmit`, and `npm run build` all pass.
|
`npm test` (821 passed), `tsc --noEmit`, and `npm run build` all pass.
|
||||||
|
|
||||||
|
### Matins ferial nocturn: mid-nocturn versicle removed, a real live-verified bug (2026-09-02)
|
||||||
|
|
||||||
|
User noticed the 1-nocturn ferial branch had a V./R. versicle stuck mid-list as if marking a
|
||||||
|
nocturn boundary — structurally incoherent for a single continuous nocturn.
|
||||||
|
|
||||||
|
`data/hours/matins-ferial-antiphons.yml`'s per-weekday `groups` carried a `versicle` field
|
||||||
|
ported from a mid-list V./R. line embedded in the source `Psalmi matutinum.txt`
|
||||||
|
(`[Daym1_]`-`[Daym6_]`), rendered partway through the single continuous 1-nocturn ferial day.
|
||||||
|
Live-verified against the reference engine (`Pofficium.pl`, `command=prayMatutinum`,
|
||||||
|
`version=Monastic Tridentinum 1617`) for Wednesday and Thursday: the real office runs straight
|
||||||
|
from the last psalm to the Capitulum, no versicle anywhere in the psalmody — that source line
|
||||||
|
is inert there, never actually emitted. Removed the field from all 6 weekdays and the
|
||||||
|
rendering code (`ferialAntiphonedNocturn`); the earlier "versicle placement" reasoning in that
|
||||||
|
file's header was a documented-but-unverified assumption, not real content.
|
||||||
|
|
||||||
|
See memory `vu_ferial_matins_antiphons` for full detail.
|
||||||
|
|
||||||
|
`npm test`, `tsc --noEmit`, and `npm run build` all pass.
|
||||||
|
|
||||||
|
### Matins: 3-nocturn threshold lowered from Duplex to Semiduplex (2026-09-02)
|
||||||
|
|
||||||
|
Same session, a separate fix: the 3-nocturn gate (`hours/matins.ts`'s `threeNocturns`) was
|
||||||
|
floored at Duplex (`isDoubleOrHigher`). User's own design call, prompted by noticing today's
|
||||||
|
Matins (St. Stephen of Hungary, Semiduplex) wrongly rendering as 1-nocturn: Semiduplex and
|
||||||
|
Duplex should share the same 3-nocturn/3-psalms-each shape (distinct from Sunday/Duplex-majus
|
||||||
|
+'s 6/6/3-plus-canticles scheme). Lowered the floor with a new, separate check
|
||||||
|
(`isAtLeast(winner.rank, 'semiduplex')`) rather than touching `isDoubleOrHigher` itself, which
|
||||||
|
drives an unrelated antiphon-doubling rule elsewhere and must stay floored at Duplex. No new
|
||||||
|
content authoring was needed — `ferialPsalmodyThreeNocturns` already existed and already
|
||||||
|
chunks the ferial 9-psalm pool into 3/3/3 as the fallback for a plain-Duplex winner with no
|
||||||
|
override authored; Semiduplex winners now fall into that same already-working branch. Proved
|
||||||
|
via St. Stephen of Hungary (2026-09-02, Common of a Confessor): 3 real nocturns, Common's own
|
||||||
|
Terce/Sext/None antiphons+versicles, Te Deum present.
|
||||||
|
|
||||||
|
See memory `vu_matins_semiduplex_threshold` for full detail.
|
||||||
|
|
||||||
|
`npm test` (823 passed), `tsc --noEmit`, and `npm run build` all pass.
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# Antiphons + versicles for the plain ferial weekday Matins nocturn (no
|
# Antiphons for the plain ferial weekday Matins nocturn (below Semiduplex,
|
||||||
# Sunday, no Duplex+ override) — previously rendered with bare psalms and
|
# see hours/matins.ts's threeNocturns threshold) — previously rendered
|
||||||
# no antiphon at all (a genuine oversight, not a documented design choice
|
# with bare psalms and no antiphon at all (a genuine oversight, not a
|
||||||
# like `ferialPsalmodyThreeNocturns`'s antiphon-less fallback).
|
# documented design choice like `ferialPsalmodyThreeNocturns`'s
|
||||||
|
# antiphon-less fallback).
|
||||||
#
|
#
|
||||||
# `psalter-distribution.yml`'s own `matins:` table is NOT the historical
|
# `psalter-distribution.yml`'s own `matins:` table is NOT the historical
|
||||||
# per-weekday psalm set — its own header documents a deliberate "editorial
|
# per-weekday psalm set — its own header documents a deliberate "editorial
|
||||||
@@ -43,16 +44,17 @@
|
|||||||
# psalm, just spread over two verse ranges); any cross-psalm pair is now
|
# psalm, just spread over two verse ranges); any cross-psalm pair is now
|
||||||
# split per the "One antiphon per psalm" note above.
|
# split per the "One antiphon per psalm" note above.
|
||||||
#
|
#
|
||||||
# Versicle placement: each weekday draws its 9 psalms from 1-2 different
|
# No mid-nocturn versicle (fixed 2026-09-02): the source `Psalmi
|
||||||
# original source days (see each weekday's own comment below for exactly
|
# matutinum.txt` files (`[Daym1_]`-`[Daym6_]`) embed a V./R. line partway
|
||||||
# which), but only one versicle fits per plain single-nocturn weekday —
|
# through each weekday's psalm list, and an earlier version of this file
|
||||||
# taken from whichever source day contributes the majority of that
|
# ported it as a real `versicle` field on the group where it happened to
|
||||||
# weekday's psalms, placed at the same relative position it held in that
|
# fall, rendered mid-nocturn as if marking a nocturn boundary. Live-verified
|
||||||
# source day's own sequence (immediately after whichever group of this
|
# against the reference engine (`Pofficium.pl`, `command=prayMatutinum`,
|
||||||
# weekday's psalms was originally its next-door neighbor, once the
|
# `version=Monastic Tridentinum 1617`) for both Wednesday and Thursday: the
|
||||||
# psalms that moved out from between them are accounted for). None of the
|
# real single-nocturn ferial office runs straight from the last psalm to
|
||||||
# cross-psalm pairs split by the 2026-09-01 change carried a versicle of
|
# the Capitulum, with no versicle anywhere in the psalmody — that embedded
|
||||||
# their own, so no versicle needed repositioning.
|
# line is inert in the source and was never actually emitted by the engine.
|
||||||
|
# Removed; do not reintroduce it without a live query proving otherwise.
|
||||||
#
|
#
|
||||||
# Not live-verified against a running query (Monastic ferial ordinary time
|
# Not live-verified against a running query (Monastic ferial ordinary time
|
||||||
# isn't independently re-derivable that way once psalter-distribution.yml's
|
# isn't independently re-derivable that way once psalter-distribution.yml's
|
||||||
@@ -62,8 +64,8 @@
|
|||||||
|
|
||||||
monday:
|
monday:
|
||||||
# All 9 psalms are Daym1_'s own (Monastic Matins Feria II) — no reslicing
|
# All 9 psalms are Daym1_'s own (Monastic Matins Feria II) — no reslicing
|
||||||
# across weekday boundaries here, so both antiphon grouping and the
|
# across weekday boundaries here, so the antiphon grouping sits exactly
|
||||||
# versicle sit exactly where Daym1_ itself has them.
|
# where Daym1_ itself has it.
|
||||||
groups:
|
groups:
|
||||||
- psalms: [{ number: 32 }]
|
- psalms: [{ number: 32 }]
|
||||||
antiphon:
|
antiphon:
|
||||||
@@ -83,9 +85,6 @@ monday:
|
|||||||
antiphon:
|
antiphon:
|
||||||
la: "Revéla * Dómino viam meam."
|
la: "Revéla * Dómino viam meam."
|
||||||
en: "Commit my way * to the Lord."
|
en: "Commit my way * to the Lord."
|
||||||
versicle:
|
|
||||||
v: { la: "Dómine, in cælo misericórdia tua.", en: "O Lord, thy mercy is in heaven," }
|
|
||||||
r: { la: "Et véritas tua usque ad nubes.", en: "And thy truth reacheth even to the clouds." }
|
|
||||||
- psalms: [{ number: 37 }]
|
- psalms: [{ number: 37 }]
|
||||||
antiphon:
|
antiphon:
|
||||||
la: "Ne in ira tua * árguas me Dómine."
|
la: "Ne in ira tua * árguas me Dómine."
|
||||||
@@ -107,11 +106,7 @@ monday:
|
|||||||
|
|
||||||
tuesday:
|
tuesday:
|
||||||
# 43-44 are Monday's own tail (Daym1_); 47-58 are Daym2_'s own opening (6
|
# 43-44 are Monday's own tail (Daym1_); 47-58 are Daym2_'s own opening (6
|
||||||
# of 9 psalms — the majority, hence Daym2_'s own versicle here); 59 is
|
# of 9 psalms — the majority); 59 is Wednesday's own opening (Daym3_).
|
||||||
# Wednesday's own opening (Daym3_). The versicle sits right after 49,
|
|
||||||
# exactly where Daym2_ itself has it (between the "49,51" and "52,53"
|
|
||||||
# groups — 51/52/53 all moved elsewhere in the reslice, but 49 and 54
|
|
||||||
# remain each other's nearest neighbor once they're gone).
|
|
||||||
groups:
|
groups:
|
||||||
- psalms: [{ number: 43 }]
|
- psalms: [{ number: 43 }]
|
||||||
# New (2026-09-01): the source's shared antiphon for this pair
|
# New (2026-09-01): the source's shared antiphon for this pair
|
||||||
@@ -138,9 +133,6 @@ tuesday:
|
|||||||
antiphon:
|
antiphon:
|
||||||
la: "Deus deórum * Dóminus locútus est."
|
la: "Deus deórum * Dóminus locútus est."
|
||||||
en: "The God of gods * the Lord hath spoken."
|
en: "The God of gods * the Lord hath spoken."
|
||||||
versicle:
|
|
||||||
v: { la: "Ímmola Deo sacrifícium laudis.", en: "Offer to God the sacrifice of praise." }
|
|
||||||
r: { la: "Et redde Altíssimo vota tua.", en: "And pay thy vows to the most High." }
|
|
||||||
- psalms: [{ number: 54 }]
|
- psalms: [{ number: 54 }]
|
||||||
antiphon:
|
antiphon:
|
||||||
la: "Inténde in me * et exáudi me."
|
la: "Inténde in me * et exáudi me."
|
||||||
@@ -162,9 +154,7 @@ tuesday:
|
|||||||
|
|
||||||
wednesday:
|
wednesday:
|
||||||
# 65-72 are Daym3_'s own (7 of 9 — the majority); 73 is Thursday's own
|
# 65-72 are Daym3_'s own (7 of 9 — the majority); 73 is Thursday's own
|
||||||
# opening (Daym4). The versicle sits between the "67ab" and "68ab"
|
# opening (Daym4).
|
||||||
# groups, exactly where Daym3_ itself has it — nothing moved out from
|
|
||||||
# between those two groups in the reslice, so the position is unchanged.
|
|
||||||
groups:
|
groups:
|
||||||
- psalms: [{ number: 65 }]
|
- psalms: [{ number: 65 }]
|
||||||
antiphon:
|
antiphon:
|
||||||
@@ -174,9 +164,6 @@ wednesday:
|
|||||||
antiphon:
|
antiphon:
|
||||||
la: "In ecclésiis, * benedícite Dóminum."
|
la: "In ecclésiis, * benedícite Dóminum."
|
||||||
en: "In the churches * bless ye the Lord."
|
en: "In the churches * bless ye the Lord."
|
||||||
versicle:
|
|
||||||
v: { la: "Benedícite, gentes, Deum nostrum.", en: "O bless our God, ye people." }
|
|
||||||
r: { la: "Et audítam fácite vocem laudis ejus.", en: "And make the voice of his praise to be heard." }
|
|
||||||
- psalms: [{ number: 68, verses: "2-16" }, { number: 68, verses: "17-37" }]
|
- psalms: [{ number: 68, verses: "2-16" }, { number: 68, verses: "17-37" }]
|
||||||
antiphon:
|
antiphon:
|
||||||
la: "Quǽrite Dóminum * et vivet ánima vestra."
|
la: "Quǽrite Dóminum * et vivet ánima vestra."
|
||||||
@@ -204,9 +191,7 @@ wednesday:
|
|||||||
|
|
||||||
thursday:
|
thursday:
|
||||||
# All 9 psalms are Daym4's own — no reslicing across weekday boundaries
|
# All 9 psalms are Daym4's own — no reslicing across weekday boundaries
|
||||||
# here either, so grouping and versicle position are both unchanged from
|
# here either, so the grouping is unchanged from the source.
|
||||||
# the source (the versicle sits between the "78" and "79,80" groups,
|
|
||||||
# exactly as Daym4 itself has it).
|
|
||||||
groups:
|
groups:
|
||||||
- psalms: [{ number: 76 }]
|
- psalms: [{ number: 76 }]
|
||||||
antiphon:
|
antiphon:
|
||||||
@@ -220,9 +205,6 @@ thursday:
|
|||||||
antiphon:
|
antiphon:
|
||||||
la: "Propítius esto * peccátis nostris, Dómine."
|
la: "Propítius esto * peccátis nostris, Dómine."
|
||||||
en: "Forgive us our sins * O Lord."
|
en: "Forgive us our sins * O Lord."
|
||||||
versicle:
|
|
||||||
v: { la: "Deus, in sancto via tua.", en: "Thy way, O God, is in the holy place." }
|
|
||||||
r: { la: "Quis Deus magnus sicut Deus noster.", en: "Who is the great God like our God?" }
|
|
||||||
- psalms: [{ number: 79 }]
|
- psalms: [{ number: 79 }]
|
||||||
# New (2026-09-01): no real antiphon found elsewhere in the corpus
|
# New (2026-09-01): no real antiphon found elsewhere in the corpus
|
||||||
# for Ps 79 alone; composed from its own opening verse (79:2).
|
# for Ps 79 alone; composed from its own opening verse (79:2).
|
||||||
@@ -254,10 +236,7 @@ friday:
|
|||||||
# elsewhere, so it's kept here on 93 alone; likewise 95's antiphon
|
# elsewhere, so it's kept here on 93 alone; likewise 95's antiphon
|
||||||
# originally also covered 96, which the redistribution's own documented
|
# originally also covered 96, which the redistribution's own documented
|
||||||
# hand-adjustment moved to Saturday to keep both halves of Ps 103
|
# hand-adjustment moved to Saturday to keep both halves of Ps 103
|
||||||
# together there); 101-103 are Daym6_'s own opening (4 of 9). The
|
# together there); 101-103 are Daym6_'s own opening (4 of 9).
|
||||||
# versicle sits between the "93" and "95" groups, exactly where Daym5_
|
|
||||||
# itself has it (originally between "92/93" and "95/96" — 96 leaving for
|
|
||||||
# Saturday doesn't touch this boundary).
|
|
||||||
groups:
|
groups:
|
||||||
- psalms: [{ number: 85 }]
|
- psalms: [{ number: 85 }]
|
||||||
antiphon:
|
antiphon:
|
||||||
@@ -271,9 +250,6 @@ friday:
|
|||||||
antiphon:
|
antiphon:
|
||||||
la: "Exaltáre, * qui júdicas terram."
|
la: "Exaltáre, * qui júdicas terram."
|
||||||
en: "Lift up thyself, * thou that judgest the earth."
|
en: "Lift up thyself, * thou that judgest the earth."
|
||||||
versicle:
|
|
||||||
v: { la: "Intret orátio mea in conspéctu tuo, Dómine.", en: "Let my prayer come before Thee, O Lord." }
|
|
||||||
r: { la: "Inclína aurem tuam ad precem meam.", en: "Incline Thine ear unto my cry." }
|
|
||||||
- psalms: [{ number: 95 }]
|
- psalms: [{ number: 95 }]
|
||||||
antiphon:
|
antiphon:
|
||||||
la: "Cantáte Dómino, * et benedícite nómini ejus."
|
la: "Cantáte Dómino, * et benedícite nómini ejus."
|
||||||
@@ -297,9 +273,7 @@ saturday:
|
|||||||
# Ps 96 is Daym5_'s own tail (moved here by the redistribution's own
|
# Ps 96 is Daym5_'s own tail (moved here by the redistribution's own
|
||||||
# documented hand-adjustment, swapped out of Friday to let both halves
|
# documented hand-adjustment, swapped out of Friday to let both halves
|
||||||
# of Ps 103 land there instead); 104-108 are Daym6_'s own tail (8 of 9 —
|
# of Ps 103 land there instead); 104-108 are Daym6_'s own tail (8 of 9 —
|
||||||
# the majority, hence Daym6_'s own versicle here). The versicle sits
|
# the majority).
|
||||||
# between the "104ab" and "105ab" groups, exactly where Daym6_ itself
|
|
||||||
# has it — nothing moved out from between those two groups.
|
|
||||||
groups:
|
groups:
|
||||||
- psalms: [{ number: 96 }]
|
- psalms: [{ number: 96 }]
|
||||||
antiphon:
|
antiphon:
|
||||||
@@ -309,9 +283,6 @@ saturday:
|
|||||||
antiphon:
|
antiphon:
|
||||||
la: "Lætétur cor * quæréntium Dóminum."
|
la: "Lætétur cor * quæréntium Dóminum."
|
||||||
en: "Let the heart of them rejoice, * that seek the Lord."
|
en: "Let the heart of them rejoice, * that seek the Lord."
|
||||||
versicle:
|
|
||||||
v: { la: "Dómine, exáudi oratiónem meam.", en: "O Lord, hear my prayer." }
|
|
||||||
r: { la: "Et clamor meus ad te véniat.", en: "And let my cry come unto thee." }
|
|
||||||
- psalms: [{ number: 105, verses: "1-31" }, { number: 105, verses: "32-48" }]
|
- psalms: [{ number: 105, verses: "1-31" }, { number: 105, verses: "32-48" }]
|
||||||
antiphon:
|
antiphon:
|
||||||
la: "Vísita nos, * Dómine, in salutári tuo."
|
la: "Vísita nos, * Dómine, in salutári tuo."
|
||||||
|
|||||||
+39
-33
@@ -3,8 +3,8 @@
|
|||||||
// Builds its ordo programmatically per day rather than resolving a static
|
// Builds its ordo programmatically per day rather than resolving a static
|
||||||
// `data/hours/matins.yml` parts list the way every other hour does, because
|
// `data/hours/matins.yml` parts list the way every other hour does, because
|
||||||
// the real shape (1 nocturn on a plain ferial day vs. 3 on a Sunday or a
|
// the real shape (1 nocturn on a plain ferial day vs. 3 on a Sunday or a
|
||||||
// Duplex+ feast, with a *variable* number of readings) doesn't fit that
|
// Semiduplex+ feast, with a *variable* number of readings) doesn't fit
|
||||||
// pattern.
|
// that pattern.
|
||||||
//
|
//
|
||||||
// Critical framing (see memory `vu-not-a-reconstruction` / `vu-matins-
|
// Critical framing (see memory `vu-not-a-reconstruction` / `vu-matins-
|
||||||
// design`, and TODO.md's own Matins section): this is NOT a historical
|
// design`, and TODO.md's own Matins section): this is NOT a historical
|
||||||
@@ -76,7 +76,7 @@ import { getPsalmVerses } from '../psalter';
|
|||||||
import { getPsalmsFor, type PsalmRef } from '../psalter/distribution';
|
import { getPsalmsFor, type PsalmRef } from '../psalter/distribution';
|
||||||
import { getScriptureVerses } from '../scripture';
|
import { getScriptureVerses } from '../scripture';
|
||||||
import { getOpeningVersicleId } from './opening-versicle';
|
import { getOpeningVersicleId } from './opening-versicle';
|
||||||
import { isDoubleOrHigher, applyFlexaMark } from './antiphon';
|
import { applyFlexaMark } from './antiphon';
|
||||||
import {
|
import {
|
||||||
resolveCommon,
|
resolveCommon,
|
||||||
getDayCollect,
|
getDayCollect,
|
||||||
@@ -208,7 +208,6 @@ function sundayNocturnFor(nocturnKey: 'nocturn1' | 'nocturn2' | 'nocturn3', day:
|
|||||||
interface FerialGroup {
|
interface FerialGroup {
|
||||||
psalms: PsalmRef[];
|
psalms: PsalmRef[];
|
||||||
antiphon: BilingualText;
|
antiphon: BilingualText;
|
||||||
versicle?: { v: BilingualText; r: BilingualText };
|
|
||||||
}
|
}
|
||||||
interface FerialNocturn {
|
interface FerialNocturn {
|
||||||
groups: FerialGroup[];
|
groups: FerialGroup[];
|
||||||
@@ -434,12 +433,14 @@ function resolveMatinsHymn(day: LiturgicalDay): ResolvedText {
|
|||||||
|
|
||||||
/** 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 were reconciled against psalter-distribution.yml's own
|
||||||
* distribution.yml's own "editorial redistribution" of the historical
|
* "editorial redistribution" of the historical per-weekday psalm set).
|
||||||
* per-weekday psalm set). Every psalm-group carries its own antiphon
|
* Every psalm-group carries its own antiphon (incipit-or-full opening,
|
||||||
* (incipit-or-full opening, same rank rule as Sunday's own nocturns), and
|
* same rank rule as Sunday's own nocturns). No versicle: an earlier
|
||||||
* exactly one group per weekday carries a versicle+responsory, rendered
|
* version of this ported a mid-list V./R. line embedded in the source
|
||||||
* as one combined V./R. block via `versicleText`. */
|
* `Psalmi matutinum.txt`, but that line is live-verified inert there — the
|
||||||
|
* real single-nocturn ferial office runs straight from the last psalm to
|
||||||
|
* the Capitulum (fixed 2026-09-02, see the data file's own header). */
|
||||||
function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] {
|
function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] {
|
||||||
const nocturn = ferialAntiphons[day.weekday as Exclude<Weekday, 'sunday'>];
|
const nocturn = ferialAntiphons[day.weekday as Exclude<Weekday, 'sunday'>];
|
||||||
const winner = resolveOfficeWinner(day);
|
const winner = resolveOfficeWinner(day);
|
||||||
@@ -459,22 +460,20 @@ function ferialAntiphonedNocturn(day: LiturgicalDay): ResolvedPart[] {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
parts.push({ kind: 'antiphon', text: full });
|
parts.push({ kind: 'antiphon', text: full });
|
||||||
if (group.versicle) {
|
|
||||||
parts.push({ kind: 'versicle', text: versicleText(group.versicle) });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fallback for a Duplex+ weekday feast with no matins-psalmody-overrides
|
/** Fallback for a Semiduplex+ weekday feast with no matins-psalmody-overrides
|
||||||
* entry authored for its winner yet: the plain ferial weekday table,
|
* entry authored for its winner yet: the plain ferial weekday table,
|
||||||
* chunked into 3 nocturns instead of 1. Bare (no antiphons, no canticles)
|
* chunked into 3 nocturns instead of 1. Bare (no antiphons, no canticles)
|
||||||
* when `common` isn't given — genuinely nothing authored at all for this
|
* when `common` isn't given — genuinely nothing authored at all for this
|
||||||
* winner, still used as-is for a duplex-majus+ winner with no Common
|
* winner, still used as-is for a duplex-majus+ winner with no Common
|
||||||
* category either.
|
* category either.
|
||||||
*
|
*
|
||||||
* When `common`/`commonId` *are* given (a duplex-rank winner whose Common
|
* When `common`/`commonId` *are* given (a Semiduplex- or Duplex-rank
|
||||||
* category exists — user, 2026-08), each nocturn borrows that category's
|
* winner whose Common category exists — user, 2026-08, threshold lowered
|
||||||
|
* to Semiduplex 2026-09-02), each nocturn borrows that category's
|
||||||
* own `nocturn{N}.versicle`: the ferial table only ever authors one
|
* own `nocturn{N}.versicle`: the ferial table only ever authors one
|
||||||
* versicle for the whole (normally 1-nocturn) day, so a 3-nocturn split
|
* versicle for the whole (normally 1-nocturn) day, so a 3-nocturn split
|
||||||
* leaves 2 of the 3 nocturns with none at all — Common's own 3 real
|
* leaves 2 of the 3 nocturns with none at all — Common's own 3 real
|
||||||
@@ -548,8 +547,8 @@ function withSingleAntiphon(refs: PsalmRef[], antiphonText: ResolvedText, winner
|
|||||||
* 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`
|
||||||
* once `resolveDay` has applied the transfer, so it needs no separate
|
* once `resolveDay` has applied the transfer, so it needs no separate
|
||||||
* lookup here), and — only on a real 3-nocturn day (Sunday, or a Duplex+
|
* lookup here), and — only on a real 3-nocturn day (Sunday, or a
|
||||||
* feast) — the plain temporal id itself (that Sunday's own Moralia-in-
|
* Semiduplex+ feast) — the plain temporal id itself (that Sunday's own Moralia-in-
|
||||||
* Job-style commentary) plus, for dates from the 1st Sunday of August
|
* Job-style commentary) plus, for dates from the 1st Sunday of August
|
||||||
* through the eve of Advent, the calendar-month/week id (`month-week-<id>`,
|
* through the eve of Advent, the calendar-month/week id (`month-week-<id>`,
|
||||||
* calendar/month-week-id.ts's monthWeekId): the real historical Nocturn 2
|
* calendar/month-week-id.ts's monthWeekId): the real historical Nocturn 2
|
||||||
@@ -735,12 +734,17 @@ 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 Duplex+ sanctoral winner — the
|
// Every Sunday, unconditionally, or a Semiduplex+ sanctoral winner — the
|
||||||
// user's own chosen threshold (2026-08), not gated on whether any
|
// user's own chosen threshold (2026-08, lowered from Duplex to
|
||||||
// content is actually authored yet, same "eligible, not content-gated"
|
// Semiduplex 2026-09-02), not gated on whether any content is actually
|
||||||
// convention every other per-feast override in this app already uses
|
// authored yet, same "eligible, not content-gated" convention every
|
||||||
// (see hours/resolve-common.ts's getOfficeOverrideId).
|
// other per-feast override in this app already uses (see
|
||||||
const threeNocturns = day.weekday === 'sunday' || isDoubleOrHigher(winner);
|
// hours/resolve-common.ts's getOfficeOverrideId). Deliberately a
|
||||||
|
// separate check from `isDoubleOrHigher` (antiphon.ts) — that function
|
||||||
|
// drives the unrelated antiphon-doubling rule (full vs. incipit antiphon
|
||||||
|
// before the psalms) and stays floored at Duplex; the two thresholds
|
||||||
|
// only coincided before this change by accident, not by shared meaning.
|
||||||
|
const threeNocturns = day.weekday === 'sunday' || (winner.kind === 'sanctoral' && isAtLeast(winner.rank, 'semiduplex'));
|
||||||
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);
|
||||||
|
|
||||||
@@ -776,19 +780,21 @@ export function resolveOrdo(date: string): ResolvedOrdo {
|
|||||||
parts.push({ kind: 'te-deum', text: resolveCommon('te-deum') });
|
parts.push({ kind: 'te-deum', text: resolveCommon('te-deum') });
|
||||||
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 Semiduplex+ weekday feast — genuinely different psalmody from a
|
||||||
// Sunday's, not the same content reused.
|
// real Sunday's, not the same content reused.
|
||||||
// - Saint-specific proper: use its own full scheme, any rank.
|
// - Saint-specific proper: use its own full scheme, any rank.
|
||||||
// - No proper, rank duplex-majus+: Common's own full scheme (own
|
// - No proper, rank duplex-majus+: Common's own full scheme (own
|
||||||
// psalm numbers, canticles) if authored; else the fully bare
|
// psalm numbers, canticles) if authored; else the fully bare
|
||||||
// ferialPsalmodyThreeNocturns stand-in. Unaffected by the
|
// ferialPsalmodyThreeNocturns stand-in. Unaffected by the
|
||||||
// duplex-and-below carve-out below (user, 2026-08).
|
// below-duplex-majus carve-out below (user, 2026-08).
|
||||||
// - No proper, rank exactly duplex (the floor of 3-nocturn
|
// - No proper, rank Semiduplex or Duplex (below the floor of the
|
||||||
// eligibility): ferial psalm numbers, not Common's own — but
|
// duplex-majus+ tier, but at or above the floor of 3-nocturn
|
||||||
// Common's per-nocturn versicle, and each nocturn's own Terce/
|
// eligibility itself, lowered to Semiduplex 2026-09-02): ferial
|
||||||
// Sext/None antiphon for that category, if authored (see
|
// psalm numbers, not Common's own — but Common's per-nocturn
|
||||||
// ferialPsalmodyThreeNocturns's own doc comment for why those
|
// versicle, and each nocturn's own Terce/Sext/None antiphon for
|
||||||
// three, not Common's own Matins Nocturn antiphons).
|
// 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 properOverride = winner.kind === 'sanctoral' ? getMatinsSaintOverride(winner.id) : undefined;
|
||||||
const isAboveDuplex = winner.kind === 'sanctoral' && isAtLeast(winner.rank, 'duplex-majus');
|
const isAboveDuplex = winner.kind === 'sanctoral' && isAtLeast(winner.rank, 'duplex-majus');
|
||||||
const commonOverride = winner.kind === 'sanctoral' && !properOverride ? getMatinsCommonOverride(winner.id) : undefined;
|
const commonOverride = winner.kind === 'sanctoral' && !properOverride ? getMatinsCommonOverride(winner.id) : undefined;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ describe('resolveOrdo("matins", ...) ferial (1-nocturn) branch', () => {
|
|||||||
expect(ordo.parts.some((p) => p.kind === 'versicle' && p.text.text.la?.includes('Te decet laus'))).toBe(false);
|
expect(ordo.parts.some((p) => p.kind === 'versicle' && p.text.text.la?.includes('Te decet laus'))).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("gives every nocturn psalm its own antiphon and includes Tuesday's own versicle (data/hours/matins-ferial-antiphons.yml)", () => {
|
it('gives every nocturn psalm its own antiphon and has no mid-nocturn versicle (data/hours/matins-ferial-antiphons.yml)', () => {
|
||||||
// The nocturn's own psalms start at index 5 (after the two opening versicles/Ps3/heading/Ps94).
|
// The nocturn's own psalms start at index 5 (after the two opening versicles/Ps3/heading/Ps94).
|
||||||
const nocturnPsalms = ordo.parts.slice(5).filter((p) => p.kind === 'psalm') as {
|
const nocturnPsalms = ordo.parts.slice(5).filter((p) => p.kind === 'psalm') as {
|
||||||
psalmNumber: number;
|
psalmNumber: number;
|
||||||
@@ -65,11 +65,11 @@ describe('resolveOrdo("matins", ...) ferial (1-nocturn) branch', () => {
|
|||||||
expect(p.antiphon.text.en).toBeTruthy();
|
expect(p.antiphon.text.en).toBeTruthy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// No versicle anywhere in the psalmody (fixed 2026-09-02) — the
|
||||||
|
// source's embedded mid-list V./R. line is live-verified inert; the
|
||||||
|
// real single-nocturn ferial office runs straight to the Capitulum.
|
||||||
const nocturnVersicles = ordo.parts.slice(5).filter((p) => p.kind === 'versicle');
|
const nocturnVersicles = ordo.parts.slice(5).filter((p) => p.kind === 'versicle');
|
||||||
expect(nocturnVersicles).toHaveLength(1);
|
expect(nocturnVersicles).toHaveLength(0);
|
||||||
const versicleText = (nocturnVersicles[0] as { text: { text: Record<string, string> } }).text.text;
|
|
||||||
expect(versicleText.la).toContain('Ímmola Deo sacrifícium laudis');
|
|
||||||
expect(versicleText.la).toContain('Et redde Altíssimo vota tua');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves the user's own bible-plan readings for the day, not the historical lectionary", () => {
|
it("resolves the user's own bible-plan readings for the day, not the historical lectionary", () => {
|
||||||
@@ -527,6 +527,49 @@ describe('resolveOrdo("matins", ...) Duplex+ weekday-feast (3-nocturn, non-Sunda
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('resolveOrdo("matins", ...) Semiduplex weekday-feast (3-nocturn threshold lowered 2026-09-02)', () => {
|
||||||
|
// St. Stephen of Hungary (Semiduplex, Common of a Confessor Not a
|
||||||
|
// Bishop, no proper Matins antiphon of his own) -- 2026-09-02, a
|
||||||
|
// Wednesday. Threshold was Duplex-or-higher before this fix, so a
|
||||||
|
// Semiduplex winner used to fall all the way through to the plain
|
||||||
|
// 1-nocturn ferial branch; the user's own design intent (2026-09-02) is
|
||||||
|
// that Semiduplex and Duplex share the same 3-nocturn/3-psalms-each
|
||||||
|
// shape, distinct from the 6/6/3-plus-canticles Sunday/Duplex-majus+
|
||||||
|
// scheme. Reuses the same already-working ferialPsalmodyThreeNocturns
|
||||||
|
// branch a plain-Duplex winner gets (see the describe block above) --
|
||||||
|
// no new content authoring was needed for this fix.
|
||||||
|
const ordo = resolveOrdo('matins', '2026-09-02');
|
||||||
|
|
||||||
|
it('splits the ferial 9-psalm pool into 3 nocturns of 3 each, plus a Te Deum, not the 1-nocturn ferial branch', () => {
|
||||||
|
const psalms = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p as { psalmNumber: number }).psalmNumber);
|
||||||
|
expect(psalms).toEqual([3, 94, 65, 67, 67, 68, 68, 70, 71, 72, 73]);
|
||||||
|
expect(ordo.parts.some((p) => p.kind === 'te-deum')).toBe(true);
|
||||||
|
// No canticles -- reserved for Sunday/Duplex-majus+, not this tier.
|
||||||
|
expect(ordo.parts.some((p) => p.kind === 'canticle')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses Common-of-a-Confessor's own Terce/Sext/None antiphons and versicles, one distinct pair per nocturn", () => {
|
||||||
|
const psalmParts = ordo.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));
|
||||||
|
expect(nocturnPsalms[0]?.antiphon?.text.la).toContain('Euge, serve bone');
|
||||||
|
expect(nocturnPsalms[3]?.antiphon?.text.la).toContain('Fidélis servus');
|
||||||
|
expect(nocturnPsalms[6]?.antiphon?.text.la).toContain('Serve bone');
|
||||||
|
for (const i of [0, 3, 6]) {
|
||||||
|
expect(nocturnPsalms[i]?.antiphon?.status.la).toBe('verified');
|
||||||
|
expect(nocturnPsalms[i]?.antiphon?.status.en).toBe('verified');
|
||||||
|
}
|
||||||
|
const versicles = ordo.parts.filter((p) => p.kind === 'versicle') as { text: { text: Record<string, string> } }[];
|
||||||
|
// The 3 nocturn-closing versicles (excluding the opening pair and the
|
||||||
|
// final Te decet laus) are Common-of-a-Confessor's own, one per
|
||||||
|
// nocturn -- not a stray mid-psalmody line.
|
||||||
|
const nocturnVersicles = versicles.slice(2, 5);
|
||||||
|
expect(nocturnVersicles).toHaveLength(3);
|
||||||
|
expect(nocturnVersicles[0]?.text.text.la).toContain('Amávit eum Dóminus');
|
||||||
|
expect(nocturnVersicles[1]?.text.text.la).toContain('Os justi meditábitur');
|
||||||
|
expect(nocturnVersicles[2]?.text.text.la).toContain('Lex Dei ejus in corde ipsíus');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('resolveOrdo("matins", ...) second Duplex+ weekday-feast proof — St. Andrew, Common of an Apostle', () => {
|
describe('resolveOrdo("matins", ...) second Duplex+ weekday-feast proof — St. Andrew, Common of an Apostle', () => {
|
||||||
// 2026-11-30 -- St. Andrew's own day outright ("S. Andreæ Apostoli",
|
// 2026-11-30 -- St. Andrew's own day outright ("S. Andreæ Apostoli",
|
||||||
// not transferred that year: Nov 30 falls on a Monday, not a Sunday),
|
// not transferred that year: Nov 30 falls on a Monday, not a Sunday),
|
||||||
|
|||||||
Reference in New Issue
Block a user