Compare commits

...

5 Commits

Author SHA1 Message Date
will 943aead9ae Document the remaining psalmody Common-category work in TODO.md
Deploy / deploy (push) Successful in 1m24s
Records which of the 24 SaintRecord.common categories are now covered
(authored or aliased), which one real content gap remains
(common-of-a-dedication's Lauds canticle text), and why
common-of-a-vigil isn't a gap at all (structurally unreachable by rank).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHXJAWVAabCKX1JgmDh4Rn
2026-08-28 21:33:22 -04:00
will ba4a5c70db Add regression tests for the remaining psalmody Common categories
Covers common-of-an-angel (both hours, via Guardian Angels' First
Vespers/own day), common-of-a-dedication (Vespers, via its own day),
and the common-of-an-evangelist/several-women-martyrs aliases (one
date-based test via St. Mark where a real date is reachable, direct
unit tests where every carrier saint is below this override's rank
threshold or shadowed by an existing per-feast override).

Two pre-existing Vespers test dates had to move again for the same
reason as the previous session's fix: newly-correct category coverage
retroactively changed what a "clean ferial"/"own kept evening" date
actually resolves to. 2026-09-28 (anticipates St. Michael's own First
Vespers) moved to 2026-10-12; Guardian Angels' own day (Oct 2) moved to
Oct 1 (her First Vespers) once tracing resolveEveningDay showed she
doesn't keep her own Second Vespers there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHXJAWVAabCKX1JgmDh4Rn
2026-08-28 21:33:14 -04:00
will fbb3a2cfc7 Alias common-of-an-evangelist and -several-women-martyrs, not new schemes
St. Mark's and St. Luke's own reference-engine rule files say "ex C1a",
byte-identical to plain C1 (Apostles) -- an Evangelist-not-Apostle
reuses the Apostles' own scheme, not a separate one.

Ss. Perpetua and Felicity's own file says "vide C7b"; C7b returns the
same four psalm numbers and group/canticle/Laudate antiphons as plain
C7 (Holy Woman) -- its only real difference is the Magnificat/Benedictus
antiphon, handled by a separate, already-existing mechanism, not this
psalmody table. (First-draft duplicate category files for this id were
written and then deleted once this was caught by diffing line-by-line
against the live output.)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHXJAWVAabCKX1JgmDh4Rn
2026-08-28 21:33:05 -04:00
will e7fc70bf77 Add Common-of-a-Dedication Vespers psalmody category
Genuinely its own scheme (votive=C8) -- unlike every other Common,
Vespers' numbers (109/110/111/147) don't match any other category's
pattern. Lauds' own version is left unauthored: its OT canticle
("Canticum Ieremiae") has never been transcribed anywhere in this app,
so that file isn't written rather than guessed at (see TODO.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHXJAWVAabCKX1JgmDh4Rn
2026-08-28 21:32:54 -04:00
will 15cc76dacf Add Common-of-an-Angel Lauds/Vespers psalmody category
No dedicated numbered Common exists for Angels in the reference engine;
St. Michael's own rule says plain "Psalmi Dominica" with his own proper
antiphons, which Guardian Angels (no proper of her own) reuses
byte-for-byte at both hours. Authored as a reusable category so any
future proper-less Angel addition benefits automatically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHXJAWVAabCKX1JgmDh4Rn
2026-08-28 21:32:46 -04:00
10 changed files with 334 additions and 20 deletions
+85 -8
View File
@@ -48,14 +48,21 @@ across every hour and content type in this app.
- The bulk Vulgate (Latin) + Douay-Rheims (English) scripture import into - The bulk Vulgate (Latin) + Douay-Rheims (English) scripture import into
`src/data/scripture/` — long-term project, explicitly deferred; Matins scripture `src/data/scripture/` — long-term project, explicitly deferred; Matins scripture
citations resolve to `missing` text until it lands. citations resolve to `missing` text until it lands.
- Lauds/Vespers duplex-majus+ Common-category psalmody: 8 of 24 `SaintRecord.common` - Lauds/Vespers duplex-majus+ Common-category psalmody: 10 of 24 `SaintRecord.common`
categories authored (covering 16 ids via aliases) for both hours — see "Lauds/Vespers categories authored (covering 19 ids via aliases) for Vespers, 9 of 24 (18 ids) for Lauds —
duplex-majus+ Common-category psalmody" below. Remaining: `common-of-a-dedication`, see "Lauds/Vespers duplex-majus+ Common-category psalmody" below and its follow-up entry.
`common-of-an-angel`, `common-of-an-evangelist`, `common-of-a-vigil`, Genuinely still open: `common-of-a-dedication`'s **Lauds** file specifically (Vespers is
`common-of-several-women-martyrs` (no votive code confirmed yet for any of these), plus done) — blocked on a real content gap, not a mechanism gap: its OT canticle ("Canticum
Vespers' own per-feast **proper** tier (`data/hours/vespers-psalmody-overrides/saints/`) Ieremiæ") has never been transcribed anywhere in this app (the 7 existing Lauds canticles,
is mechanism-only, zero saints authored — unlike Lauds, no majus+ saint currently on the `hours/lauds-canticles.ts`, are only the plain weekday rotation) — needs sourcing before
calendar is known to have proper Vespers psalm antiphons distinct from their Common's. that one file can be authored without guessing. `common-of-a-vigil` is not a gap at all:
the `vigil` `FeastClass` sits structurally below `duplex-majus` on the rank ladder (see
`calendar/commemorations.ts`'s `FEAST_CLASS_ORDER`), so `getOfficeOverrideId` can never
return a Vigil's id — this category is permanently unreachable by design, not worth
authoring. Vespers' own per-feast **proper** tier
(`data/hours/vespers-psalmody-overrides/saints/`) is mechanism-only, zero saints authored —
unlike Lauds, no majus+ saint currently on the calendar is known to have proper Vespers
psalm antiphons distinct from their Common's.
3. **Known, deliberate simplifications** — intentional, not bugs, no plan to revisit unless 3. **Known, deliberate simplifications** — intentional, not bugs, no plan to revisit unless
priorities change: priorities change:
@@ -3997,3 +4004,73 @@ Monday") had to move to a different date: that Monday turned out to be the closi
Lawrence's own octave, which now correctly picks up Common-of-a-Martyr's psalms instead of the Lawrence's own octave, which now correctly picks up Common-of-a-Martyr's psalms instead of the
plain ferial set it was (incidentally, not intentionally) relying on before this mechanism plain ferial set it was (incidentally, not intentionally) relying on before this mechanism
existed. `npm test` (535 passed) and `npm run build` both pass. existed. `npm test` (535 passed) and `npm run build` both pass.
### Lauds/Vespers duplex-majus+ Common-category psalmody — remaining categories authored, one real gap surfaced (2026-08-28)
Follow-up to the previous entry, closing most of the "8 of 24" gap it left open. Went
category by category through the reference engine's votive-code space (the full `CommuneM/`
directory listing, `C1` through `C12` and their season-variant suffixes) to identify each
remaining `SaintRecord.common` value's real scheme, rather than assuming one exists:
- **`common-of-an-evangelist` is an alias, not its own scheme.** St. Mark's and St. Luke's own
reference-engine rule files both say "ex C1a", and `votive=C1a` returned byte-identical
Lauds/Vespers psalmody to plain `votive=C1` — an Evangelist-not-Apostle genuinely reuses the
Apostles' own scheme in the Monastic tradition. Registered as an alias on
`common-of-an-apostle`'s existing category files for both hours.
- **`common-of-several-women-martyrs` is also an alias, not its own scheme** — but only once the
actual Vespers/Lauds *psalmody* slots are isolated from the Magnificat/Benedictus antiphon
that follows them. Ss. Perpetua and Felicity's own file says "vide C7b"; `votive=C7b` returned
the *same* four psalm numbers and group/canticle/Laudate antiphons as plain `votive=C7`
(Common-of-a-Holy-Woman) for both hours — C7b's only real difference is its own proper
Magnificat/Benedictus antiphon, which belongs to a separate, already-existing mechanism
(`getMagnificatAntiphon`/`getBenedictusAntiphon`), not the psalmody-override table this pass
is building. First-draft category files for this id were written as full duplicates before
this was caught by diffing against the live output line-by-line — deleted in favor of an
alias on `common-of-a-holy-woman`'s existing files instead. Neither this id nor
`common-of-a-vigil` (see below) can currently be exercised via `resolveOrdo`/a real calendar
date (Perpetua and Felicity are plain Duplex, below this override's threshold, and no other
saint carries this Common) — verified via a direct `getLaudsCommonOverride`/
`getVespersCommonOverride` unit test instead of a date-based one.
- **`common-of-an-angel` has no real numbered Common in the source at all** — confirmed via the
full `CommuneM/` directory listing (no angel-specific code exists) and St. Michael's own
SanctiM rule, which says plain "Psalmi Dominica" (Sunday's own Vespers psalm numbers, 109-112,
not a Common-specific set) rather than citing any `Cx` file. Live-verified that Guardian
Angels (Oct 2, Duplex Majus, no proper text of her own) reuses Michael's own antiphons
byte-for-byte at both Lauds and Vespers — i.e. Michael's own proper *is* the de facto generic
Angel scheme any other Angel-Common saint without their own proper falls back to. (St. Gabriel
and St. Raphael each have their own genuinely distinct proper antiphons, matching this file's
own earlier finding for the *office bundle* that Guardian Angels/Gabriel/Raphael's
chapter/responsory/hymn/versicle content is duplicated per-saint from St. Michael's Apparition,
not drawn from a shared Common — the same absence of a real Angel Common, independently
confirmed for a different content type.) Authored as a genuine `common-of-an-angel` category
(an app-side generalization the source data doesn't literally have a file for, but a real,
reusable scheme in practice) rather than duplicating Michael's antiphons into a
Guardian-Angels-specific file, so any future proper-less Angel addition benefits automatically.
Lauds keeps the usual fixed 92/99/62 + Canticum Trium Puerorum numbers; Vespers substitutes
"Psalmi Dominica" (109-112) instead of a Common-specific set.
- **`common-of-a-dedication` is genuinely its own scheme** (`votive=C8`) for both hours — unlike
every other Common authored so far, Vespers' numbers (109/110/111/147) don't share a pattern
with any other category, and Lauds' (50/87/89 + "Canticum Ieremiæ") aren't even the usual
fixed 92/99/62 set. **Only the Vespers file was authored**: Lauds' own canticle text has never
been transcribed anywhere in this app (the 7 existing Lauds OT canticles, `hours/
lauds-canticles.ts`, are only the plain weekday rotation) — left open rather than guessed at,
see the Open-work list above.
- **`common-of-a-vigil` is not an authorable gap at all.** The `vigil` `FeastClass` sits
structurally below `duplex-majus` on `calendar/commemorations.ts`'s `FEAST_CLASS_ORDER`
ladder, so `getOfficeOverrideId`'s `isAtLeast(winner.rank, 'duplex-majus')` test can never
return true for a Vigil — this category is permanently unreachable by the mechanism as gated,
not a missing-content gap to close later.
Regression tests added to both `lauds.test.ts` and `vespers.test.ts`: a real calendar date for
each reachable category (Guardian Angels for Angel, both hours; the Dedication's own day for
Vespers), a real date via the alias for `common-of-an-evangelist` on Vespers (St. Mark, whose
own per-feast Lauds override shadows the category tier for Lauds specifically, so that one used
a direct unit test instead), and direct unit tests for the two currently-unreachable-by-date
aliases (`common-of-several-women-martyrs` for both hours). Two more pre-existing Vespers test
dates needed to move for the same reason as the previous entry's `2026-08-17` fix:
`2026-09-28` (anticipates St. Michael's own First Vespers, once his Common-of-an-Angel override
started applying) moved to `2026-10-12`; Guardian Angels' own initial test date (Oct 2, her own
day) had to move to Oct 1 (her First Vespers) once tracing `resolveEveningDay` showed she
doesn't keep her own Second Vespers (falls short of the `duplex-2-classis` floor) and Oct 3 (St.
Therese, plain Duplex, below this override's own threshold) claims her evening instead. `npm
test` (542 passed), `tsc --noEmit`, and `npm run build` all pass.
@@ -9,8 +9,16 @@
# votive=C7 is the shared source for both ids' existing matins-hymn-*.yml # votive=C7 is the shared source for both ids' existing matins-hymn-*.yml
# office-bundle files (see matins-hymn-common-of-a-widow.yml's own # office-bundle files (see matins-hymn-common-of-a-widow.yml's own
# header). # header).
#
# Also registers `common-of-several-women-martyrs`: Ss. Perpetua and
# Felicity's own reference-engine file says "vide C7b", and `votive=C7b`
# returned these exact same psalm numbers and group/canticle/Laudate
# antiphons (2026-08-28) -- C7b's only real difference from plain C7 is
# its own proper Benedictus antiphon ("Istárum est enim regnum
# cælórum..."), which belongs to a separate mechanism
# (getBenedictusAntiphon/`benedictusCommon`), not this psalmody table.
id: common-of-a-holy-woman id: common-of-a-holy-woman
aliases: [common-of-a-widow] aliases: [common-of-a-widow, common-of-several-women-martyrs]
groups: groups:
- psalms: [92] - psalms: [92]
antiphon: antiphon:
@@ -0,0 +1,41 @@
# Generic Common-of-an-Angel Lauds psalmody. Unlike every other Common
# here, there is no dedicated numbered "Cx" Common file for Angels in the
# Monastic reference engine at all (checked the full CommuneM directory
# listing) -- St. Michael's own SanctiM rule instead says plain "Psalmi
# Dominica" (Sunday's own psalmody) and carries his own proper antiphons
# directly. Live-verified (2026-08-28) that Guardian Angels (Oct 2, no
# proper text of her own) reuses Michael's own antiphons byte-for-byte at
# both Lauds and Vespers -- i.e. Michael's proper *is* the de facto
# generic Angel scheme any other Angel-Common saint without proper text
# of their own falls back to. St. Gabriel and St. Raphael each have their
# own genuinely distinct proper antiphons (not this generic scheme, and
# not each other's) -- not modeled here since a per-feast Lauds override
# would be the correct home for those, not this category.
#
# Psalm numbers here (92, 99, 62, Canticum Trium Puerorum) are still the
# usual fixed Duplex-majus+ set, confirmed against Michael's own live
# Lauds -- only Vespers substitutes "Psalmi Dominica" (109-112) instead,
# see the Vespers category file's own note.
id: common-of-an-angel
groups:
- psalms: [92]
antiphon:
la: "Stetit Angelus * iuxta aram templi, habens thuríbulum áureum in manu sua."
en: "An Angel stood at the Altar * of the Temple, having a golden censer in his hand."
- psalms: [99]
antiphon:
la: "Dum præliarétur * Míchaël Archángelus cum dracóne, audíta est vox dicéntium: Salus Deo nostro, allelúia."
en: "While as the Archangel Michael fought against the dragon, * I heard voices saying: Salvation unto our God. Alleluia."
- psalms: [62]
antiphon:
la: "Archángele Míchaël, * constítui te príncipem super omnes ánimas suscipiéndas."
en: "Michael Mine Archangel, * I have appointed thee for a prince over the ingathering of souls."
canticle:
id: canticum-trium-puerorum
antiphon:
la: "Angeli Dómini, * Dóminum benedícite in ætérnum."
en: "O ye Angels of the Lord, * bless ye the Lord always."
laudate:
antiphon:
la: "Angeli, Archángeli, * Throni et Dominatiónes, Principátus et Potestátes, Virtútes cælórum, laudáte Dóminum de cælis, allelúia."
en: "O ye Angels and Archangels, * O ye Thrones and Dominions, O ye Principalities and Powers, O ye mighty Ones of heaven, praise ye the Lord from the heavens. Alleluia."
@@ -9,8 +9,14 @@
# group of Apostles, e.g. Ss. Philip and James) under this same scheme -- # group of Apostles, e.g. Ss. Philip and James) under this same scheme --
# both ids' existing lauds-capitulum-*.yml office-bundle files already # both ids' existing lauds-capitulum-*.yml office-bundle files already
# cite the identical `votive=C1` source, so this is one scheme, not two. # cite the identical `votive=C1` source, so this is one scheme, not two.
#
# Also registers `common-of-an-evangelist`: St. Mark's and St. Luke's own
# reference-engine rule files both say "ex C1a", and `votive=C1a` returned
# byte-identical Lauds/Vespers psalmody to plain `votive=C1` (2026-08-28)
# -- an Evangelist-not-Apostle genuinely reuses the Apostles' own scheme
# in the Monastic tradition, not a separate Evangelist Common.
id: common-of-an-apostle id: common-of-an-apostle
aliases: [common-of-apostles] aliases: [common-of-apostles, common-of-an-evangelist]
groups: groups:
- psalms: [92] - psalms: [92]
antiphon: antiphon:
@@ -0,0 +1,30 @@
# Generic Common-of-a-Dedication (of a Church) Vespers psalmody, live-
# verified against Divinum Officium (Monastic Tridentinum 1617, command=
# prayVespera, votive=C8, 2026-08-28). Genuinely its own scheme, unlike
# every other Common authored so far: 109/110/111/147 (Ps 147, not the
# expected next-in-sequence 112, for the 4th slot).
#
# NOTE: no matching Lauds category file exists yet -- Lauds' own
# Dedication scheme (votive=C8, live-verified the same session) uses Ps
# 50/87/89 plus a "Canticum Ieremiæ" this app has never transcribed (the
# 7 existing Lauds OT canticles, hours/lauds-canticles.ts, are only the
# plain weekday rotation) -- a real content gap, not an oversight, left
# open rather than guessed at. See TODO.md.
id: common-of-a-dedication
groups:
- psalms: [109]
antiphon:
la: "Domum tuam, Dómine, * decet sanctitúdo in longitúdinem diérum."
en: "Holiness becometh thine house, * O Lord, for ever."
- psalms: [110]
antiphon:
la: "Domus mea, * domus oratiónis vocábitur."
en: "My house * shall be called the house of prayer."
- psalms: [111]
antiphon:
la: "Hæc est domus Dómini * fírmiter ædificáta, bene fundáta est supra firmam petram."
en: "This is the Lord's house * stoutly builded, well founded upon a sure rock."
- psalms: [147]
antiphon:
la: "Lápides pretiósi * omnes muri tui, et turres Ierúsalem gemmis ædificabúntur."
en: "All thy walls are of stones most precious, * and the towers of Jerusalem shall be built up with jewels."
@@ -9,8 +9,16 @@
# votive=C7 is the shared source for both ids' existing matins-hymn-*.yml # votive=C7 is the shared source for both ids' existing matins-hymn-*.yml
# office-bundle files (see matins-hymn-common-of-a-widow.yml's own # office-bundle files (see matins-hymn-common-of-a-widow.yml's own
# header). # header).
#
# Also registers `common-of-several-women-martyrs`: Ss. Perpetua and
# Felicity's own reference-engine file says "vide C7b", and `votive=C7b`
# returned these exact same four psalm numbers and antiphons (2026-08-28)
# -- C7b's only real difference from plain C7 is its own proper
# Magnificat antiphon ("Istárum est enim regnum cælórum..."), which
# belongs to a separate mechanism (getMagnificatAntiphon), not this
# psalmody table.
id: common-of-a-holy-woman id: common-of-a-holy-woman
aliases: [common-of-a-widow] aliases: [common-of-a-widow, common-of-several-women-martyrs]
groups: groups:
- psalms: [109] - psalms: [109]
antiphon: antiphon:
@@ -0,0 +1,32 @@
# Generic Common-of-an-Angel Vespers psalmody. Unlike every other Common
# here, there is no dedicated numbered "Cx" Common file for Angels in the
# Monastic reference engine (checked the full CommuneM directory listing)
# -- St. Michael's own SanctiM rule instead says plain "Psalmi Dominica",
# i.e. Sunday's own Vespers psalm numbers (109-112, not a Common-specific
# set), carrying his own proper antiphons directly. Live-verified
# (2026-08-28) that Guardian Angels (Oct 2, no proper text of her own)
# reuses Michael's own antiphons byte-for-byte -- i.e. Michael's proper
# *is* the de facto generic Angel scheme any other Angel-Common saint
# without proper text of their own falls back to. St. Gabriel and St.
# Raphael each have their own genuinely distinct proper antiphons (not
# this generic scheme, and not each other's) -- not modeled here since a
# per-feast Vespers override would be the correct home for those, not
# this category.
id: common-of-an-angel
groups:
- psalms: [109]
antiphon:
la: "Stetit Angelus * iuxta aram templi, habens thuríbulum áureum in manu sua."
en: "An Angel stood at the Altar * of the Temple, having a golden censer in his hand."
- psalms: [110]
antiphon:
la: "Dum præliarétur * Míchaël Archángelus cum dracóne, audíta est vox dicéntium: Salus Deo nostro, allelúia."
en: "While as the Archangel Michael fought against the dragon, * I heard voices saying: Salvation unto our God. Alleluia."
- psalms: [111]
antiphon:
la: "Archángele Míchaël, * constítui te príncipem super omnes ánimas suscipiéndas."
en: "Michael Mine Archangel, * I have appointed thee for a prince over the ingathering of souls."
- psalms: [112]
antiphon:
la: "Angeli, Archángeli, * Throni et Dominatiónes, Principátus et Potestátes, Virtútes cælórum, laudáte Dóminum de cælis, allelúia."
en: "O ye Angels and Archangels, * O ye Thrones and Dominions, O ye Principalities and Powers, O ye mighty Ones of heaven, praise ye the Lord from the heavens. Alleluia."
@@ -6,8 +6,14 @@
# group of Apostles, e.g. Ss. Philip and James) under this same scheme -- # group of Apostles, e.g. Ss. Philip and James) under this same scheme --
# both ids' existing vespers-hymn-*.yml office-bundle files already cite # both ids' existing vespers-hymn-*.yml office-bundle files already cite
# the identical `votive=C1` source, so this is one scheme, not two. # the identical `votive=C1` source, so this is one scheme, not two.
#
# Also registers `common-of-an-evangelist`: St. Mark's and St. Luke's own
# reference-engine rule files both say "ex C1a", and `votive=C1a` returned
# byte-identical Lauds/Vespers psalmody to plain `votive=C1` (2026-08-28)
# -- an Evangelist-not-Apostle genuinely reuses the Apostles' own scheme
# in the Monastic tradition, not a separate Evangelist Common.
id: common-of-an-apostle id: common-of-an-apostle
aliases: [common-of-apostles] aliases: [common-of-apostles, common-of-an-evangelist]
groups: groups:
- psalms: [109] - psalms: [109]
antiphon: antiphon:
+44
View File
@@ -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 { getLaudsCommonOverride } from '../../src/hours/lauds-psalmody-overrides';
// Clean ferial dates, one per weekday — see data/psalter-distribution.yml // Clean ferial dates, one per weekday — see data/psalter-distribution.yml
// and src/hours/lauds.ts's own generation-script provenance comments for // and src/hours/lauds.ts's own generation-script provenance comments for
@@ -361,6 +362,49 @@ describe('resolveOrdo("lauds", ...)', () => {
expect(numbers).toEqual([66, 50, 63, 64, 148, 149, 150]); expect(numbers).toEqual([66, 50, 63, 64, 148, 149, 150]);
}); });
it("uses Common-of-an-Angel's own psalmody (reusing St. Michael's own proper text) on Guardian Angels' own day", () => {
// Guardian Angels (Oct 2, Duplex Majus) has no proper text of her own
// -- live-verified (2026-08-28) that her real Lauds antiphons are St.
// Michael's own, byte-for-byte, which is exactly what this category's
// content is (see data/hours/lauds-psalmody-overrides/categories/
// common-of-an-angel.yml's own header). Psalm numbers stay the usual
// fixed Duplex-majus+ set (92, 99, 62), unlike this Common's Vespers
// psalmody, which substitutes "Psalmi Dominica" instead.
const ordo = resolveOrdo('lauds', '2026-10-02');
const numbers = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
expect(numbers).toEqual([66, 92, 99, 62, 148, 149, 150]);
const secondPsalm = ordo.parts.filter((p) => p.kind === 'psalm')[1];
expect(secondPsalm?.kind === 'psalm' ? secondPsalm.antiphon?.text.la : undefined).toContain('Stetit Angelus');
});
it('aliases common-of-an-evangelist onto common-of-an-apostle for Lauds psalmody', () => {
// Every Evangelist on this calendar (St. John Apostle, St. Luke, St.
// Mark) already has their own per-feast Lauds override authored, so
// the Common-category tier can never actually surface via
// resolveOrdo/a real date for Lauds specifically (unlike Vespers, see
// vespers.test.ts's own St. Mark test) -- test the alias wiring
// directly instead. St. Mark's and St. Luke's own reference-engine
// rule files both say "ex C1a", and votive=C1a returned byte-identical
// Lauds psalmody to plain votive=C1 (2026-08-28).
const override = getLaudsCommonOverride('st-mark');
expect(override?.id).toBe('common-of-an-apostle');
expect(override?.groups.map((g) => g.psalms)).toEqual([[92], [99], [62]]);
});
it('aliases common-of-several-women-martyrs onto common-of-a-holy-woman for Lauds psalmody', () => {
// No majus+ saint on this calendar actually carries this Common yet
// (Ss. Perpetua and Felicity, the only one, are plain Duplex -- below
// this override's threshold), so this can't be exercised via
// resolveOrdo/a real date; test the alias wiring directly instead.
// Ss. Perpetua and Felicity's own reference-engine file says "vide
// C7b", and votive=C7b returned these same groups/canticle/Laudate
// antiphons as plain votive=C7 (2026-08-28) -- only the Benedictus
// antiphon differs, a separate mechanism from this psalmody table.
const override = getLaudsCommonOverride('ss-perpetua-and-felicity');
expect(override?.id).toBe('common-of-a-holy-woman');
expect(override?.groups.map((g) => g.psalms)).toEqual([[92], [99], [62]]);
});
it("resolves St. Gregory the Great's own proper chapter/hymn, not a Common fallback", () => { it("resolves St. Gregory the Great's own proper chapter/hymn, not a Common fallback", () => {
// Live-verified against Divinum Officium (Monastic Tridentinum // Live-verified against Divinum Officium (Monastic Tridentinum
// 1617), 2033-03-12 (clean year). See lauds-capitulum-st-gregory- // 1617), 2033-03-12 (clean year). See lauds-capitulum-st-gregory-
+70 -8
View File
@@ -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 { getVespersCommonOverride } from '../../src/hours/vespers-psalmody-overrides';
// Every id with an authored Vespers office override (2026-08 sweep) — // Every id with an authored Vespers office override (2026-08 sweep) —
// same ids, same clean per-saint dates already established for each // same ids, same clean per-saint dates already established for each
@@ -75,14 +76,15 @@ describe('resolveOrdo("vespers", ...)', () => {
}); });
it('joins Ps 115 and 116 under one shared antiphon on Monday', () => { it('joins Ps 115 and 116 under one shared antiphon on Monday', () => {
// Not 2026-08-17: that Monday is actually the closing day of St. // Not 2026-08-17 (the closing day of St. Lawrence's own octave, Aug
// Lawrence's own octave (Aug 10-17, Duplex II. classis) -- now that // 10-17, Duplex II. classis) and not 2026-09-28 either (that Monday
// Vespers has its own duplex-majus+ psalmody override (2026-08-28), // evening anticipates St. Michael's own First Vespers, Sept 29,
// that evening correctly gets Common-of-a-Martyr's own psalms // Duplex I. classis) -- now that Vespers has its own duplex-majus+
// instead of the plain ferial Monday set, same as Lauds' own // psalmody override (2026-08-28), both evenings correctly pick up a
// octave-day override already did. 2026-09-28 is a Monday clear of // Common's own psalms instead of the plain ferial Monday set, same as
// any such collision. // Lauds' own octave-day override already did. 2026-10-12 is a Monday
const ordo = resolveOrdo('vespers', '2026-09-28'); // clear of any such collision.
const ordo = resolveOrdo('vespers', '2026-10-12');
const psalmParts = ordo.parts.filter((p) => p.kind === 'psalm'); const psalmParts = ordo.parts.filter((p) => p.kind === 'psalm');
const numbers = psalmParts.map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined)); const numbers = psalmParts.map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
expect(numbers).toEqual([113, 114, 115, 116, 128]); expect(numbers).toEqual([113, 114, 115, 116, 128]);
@@ -155,6 +157,66 @@ describe('resolveOrdo("vespers", ...)', () => {
expect(numbers).toEqual([109, 111, 112, 131]); expect(numbers).toEqual([109, 111, 112, 131]);
}); });
it("uses Common-of-an-Angel's own psalmody (reusing St. Michael's own proper text) on Guardian Angels' First Vespers", () => {
// Not Guardian Angels' own day itself (Oct 2, Duplex Majus): she
// falls short of keepsOwnSecondVespers's duplex-2-classis+ floor, and
// Oct 3 (St. Therese of Lisieux, plain Duplex, below this override's
// own threshold) claims First Vespers over her own evening, so Oct 2
// itself resolves to the plain ferial Friday default instead. Oct 1
// (St. Remigius, Simplex, doesn't keep his own evening either)
// anticipates Guardian Angels' own First Vespers instead, which does
// carry the override. Live-verified (2026-08-28) that her real
// Vespers antiphons are St. Michael's own, byte-for-byte, which is
// exactly what this category's content is (see data/hours/
// vespers-psalmody-overrides/categories/common-of-an-angel.yml's own
// header). Psalm numbers here are "Psalmi Dominica" (109-112), not a
// Common-specific set.
const ordo = resolveOrdo('vespers', '2026-10-01');
const numbers = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
expect(numbers).toEqual([109, 110, 111, 112]);
const firstPsalm = ordo.parts.find((p) => p.kind === 'psalm');
expect(firstPsalm?.kind === 'psalm' ? firstPsalm.antiphon?.text.la : undefined).toContain('Stetit Angelus');
});
it("uses Common-of-a-Dedication's own psalmody on the Dedication of the Basilica of the Most Holy Saviour's own day", () => {
// Nov 9, Duplex II. classis -- keeps its own evening regardless of
// tomorrow (Ss. Tryphon and Companions, Simplex). Live-verified
// against Divinum Officium (Monastic Tridentinum 1617, votive=C8),
// 2026-08-28 -- genuinely its own scheme, not shared with any other
// Common (see the category file's own header).
const ordo = resolveOrdo('vespers', '2026-11-09');
const numbers = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
expect(numbers).toEqual([109, 110, 111, 147]);
});
it("uses Common-of-an-Apostle's own psalmody on St. Mark Evangelist's own day, via the common-of-an-evangelist alias", () => {
// St. Mark (Apr 25, Duplex II. classis, common: common-of-an-
// evangelist) has no per-feast Vespers override authored -- his own
// reference-engine rule file says "ex C1a", and votive=C1a returned
// byte-identical Lauds/Vespers psalmody to plain votive=C1
// (2026-08-28), so common-of-an-evangelist is aliased onto
// common-of-an-apostle rather than duplicated (see
// data/hours/vespers-psalmody-overrides/categories/
// common-of-an-apostle.yml's own header).
const ordo = resolveOrdo('vespers', '2026-04-25');
const numbers = ordo.parts.filter((p) => p.kind === 'psalm').map((p) => (p.kind === 'psalm' ? p.psalmNumber : undefined));
expect(numbers).toEqual([109, 112, 115, 138]);
});
it('aliases common-of-several-women-martyrs onto common-of-a-holy-woman for Vespers psalmody', () => {
// No majus+ saint on this calendar actually carries this Common yet
// (Ss. Perpetua and Felicity, the only one, are plain Duplex --
// below this override's threshold), so this can't be exercised via
// resolveOrdo/a real date; test the alias wiring directly instead.
// Ss. Perpetua and Felicity's own reference-engine file says "vide
// C7b", and votive=C7b returned these same four psalm numbers/
// antiphons as plain votive=C7 (2026-08-28) -- only the Magnificat
// antiphon differs, a separate mechanism from this psalmody table.
const override = getVespersCommonOverride('ss-perpetua-and-felicity');
expect(override?.id).toBe('common-of-a-holy-woman');
expect(override?.groups.map((g) => g.psalms)).toEqual([[109], [112], [121], [126]]);
});
it('stays on the plain ferial default for a below-threshold feast (Semiduplex), even on its own kept evening', () => { it('stays on the plain ferial default for a below-threshold feast (Semiduplex), even on its own kept evening', () => {
// 2026-08-08 (Ss. Cyriacus/Largus/Smaragdus, Semiduplex) is below the // 2026-08-08 (Ss. Cyriacus/Largus/Smaragdus, Semiduplex) is below the
// duplex-majus+ override threshold -- confirmed live against Divinum // duplex-majus+ override threshold -- confirmed live against Divinum