Fix Vespers ferial psalmody weekday bug, add duplex-majus+ Common psalmody override
Deploy / deploy (push) Successful in 1m33s

Vespers psalms on Mon-Sat were silently following the anticipated
First-Vespers day's weekday instead of the actual calendar date's, since
resolvePsalmody reused resolveEveningDay's result (correct for
propers/office, which legitimately borrow tomorrow's identity, but wrong
for the ferial psalm cycle, which has no per-feast override of its own).

Also adds a duplex-majus+ Common-category psalmody override to both
Lauds and Vespers, mirroring Matins' existing saint/category mechanism.
Lauds already had a per-feast-only override; Vespers had none. 8 of 24
Common categories authored for both hours, live-verified against the
reference engine.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHXJAWVAabCKX1JgmDh4Rn
This commit is contained in:
2026-08-28 20:51:06 -04:00
parent 25934865b1
commit 33c9b2415b
22 changed files with 835 additions and 21 deletions
+82
View File
@@ -48,6 +48,14 @@ across every hour and content type in this app.
- The bulk Vulgate (Latin) + Douay-Rheims (English) scripture import into
`src/data/scripture/` — long-term project, explicitly deferred; Matins scripture
citations resolve to `missing` text until it lands.
- Lauds/Vespers duplex-majus+ Common-category psalmody: 8 of 24 `SaintRecord.common`
categories authored (covering 16 ids via aliases) for both hours — see "Lauds/Vespers
duplex-majus+ Common-category psalmody" below. Remaining: `common-of-a-dedication`,
`common-of-an-angel`, `common-of-an-evangelist`, `common-of-a-vigil`,
`common-of-several-women-martyrs` (no votive code confirmed yet for any of these), plus
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
priorities change:
@@ -3915,3 +3923,77 @@ separate `ordo-part` blocks that could be visually or structurally pulled apart.
`tests/hours/matins.test.ts`'s Sunday proof (2026-09-06, Luke 7:11-16 + St. Augustine's homily)
updated to assert against the new `gospel`/`homily` shape instead of two `isGospel`-flagged
lessons. `npm test`/`npm run build` both pass.
### Vespers ferial psalmody weekday bug — fixed (2026-08-28)
Vespers psalms on Monday-Saturday were silently following the *anticipated* (First-Vespers)
day's weekday instead of the actual calendar date's weekday, on any evening where tomorrow
claimed First Vespers (`hasFirstVespers` has no rank floor, so this fired on most such
evenings). Root cause: `hours/vespers.ts`'s `resolvePsalmody` reused `resolveEveningDay`'s
result — correct for propers/office (chapter/responsory/hymn/versicle/collects/Magnificat
antiphon), which legitimately borrow tomorrow's identity — but the ferial psalm-distribution
table has no per-feast override of its own, so it needs today's own place in the fixed 6-day
cycle regardless of whose office governs the evening. Fixed by computing `weekdayOf(date)`
(the actual calendar weekday, `calendar/weekday.ts`) separately in `resolveOrdo` and threading
it through as its own parameter, used only for the psalm-table lookup; `resolveOffice`'s
ferial-bundle fallback correctly keeps using the governing (possibly-anticipated) day's
weekday, since that IS a legitimate per-office-bundle rule. Regression test:
`tests/hours/vespers.test.ts`, 2026-08-25 (Tuesday anticipating St. Zephyrinus's Aug 26 First
Vespers) — expects Tuesday's psalm group `[129, 130, 131, 132]`, not Wednesday's. `npm
test`/`npm run build` both pass.
### Lauds/Vespers duplex-majus+ Common-category psalmody — mechanism built, 8 categories authored (2026-08-28)
Prompted by a design question: does it make sense to gate a Vespers psalm override at
duplex-majus+, the same threshold Matins already uses? Checked against the live reference
engine rather than assuming, and found two real, previously-undiscovered facts:
- **Lauds already has this override** (`getPsalmodyOverrideFor`, duplex-majus+-gated), but only
a per-feast tier (`lauds-psalmody-overrides/*.yml`) — no per-Common fallback, so most
duplex-majus+ saints without their own authored proper fell straight through to the plain
ferial default even though the real rubric calls for an override. Confirmed live
(`votive=C1`..`C11`) that Lauds' override psalm *numbers* (92, 99, 62, Canticum Trium
Puerorum) are identical across every Common — only the antiphons vary — matching the existing
per-feast overrides' own doc comments (e.g. `st-lawrence.yml`'s note that its numbers "happen
to be the same 92/99/62... that most Common-of-Martyrs-shaped feasts reuse").
- **Vespers had no psalmody-override mechanism at all** (flagged honestly in its own doc
comments as a known gap) — and unlike Lauds, its override psalm *numbers themselves* differ
by Common (Common-of-an-Apostle: 109/112/115/138; Common-of-a-Confessor-Bishop:
109/111/112/131; etc.), confirmed live the same way.
Mechanism, mirroring `matins-psalmody-overrides.ts`'s existing saint/category two-tier
precedent: `hours/lauds-psalmody-overrides.ts` gained a `categories` tier (`data/hours/
lauds-psalmody-overrides/categories/*.yml`, keyed by `SaintRecord.common` with optional
`aliases`) consulted after the existing per-feast tier — `getLaudsPsalmodyOverride` now does
`getLaudsSaintOverride(id) ?? getLaudsCommonOverride(id)`, so `hours/lauds.ts` needed no changes
at all. `hours/vespers-psalmody-overrides.ts` is a new file built from scratch with the
identical saint/category shape (the saint tier is mechanism-only, zero files yet), wired into
`hours/vespers.ts`'s `resolvePsalmody` via the same `getOfficeOverrideId` eligibility test
`resolveOfficeBundle` already uses for the chapter/hymn bundle — eligibility follows the
evening's *governing* day (so a First Vespers of a majus+ feast gets that feast's own psalms,
same as it gets that feast's own office), while the plain-ferial-fallback path still uses the
actual calendar weekday (see the bug fix above).
**8 of 24 `SaintRecord.common` categories authored for both hours** (16 ids via aliases), each
live-verified against Divinum Officium (Monastic Tridentinum 1617, `votive=C1`/`C2`/`C3`/`C4`/
`C5`/`C6`/`C7`/`C11`, 2026-08-28): `common-of-an-apostle` (+`common-of-apostles`),
`common-of-a-martyr` (+`common-of-a-martyr-bishop`), `common-of-several-martyrs`,
`common-of-a-confessor-bishop` (+`common-of-a-confessor-doctor`),
`common-of-a-confessor-not-bishop` (+`common-of-a-confessor`, `common-of-an-abbot`,
`common-of-several-confessors`), `common-of-a-virgin` (+`common-of-a-virgin-martyr`),
`common-of-a-holy-woman` (+`common-of-a-widow`), `common-of-the-bvm`. Remaining 5 categories
(`common-of-a-dedication`, `common-of-an-angel`, `common-of-an-evangelist`,
`common-of-a-vigil`, `common-of-several-women-martyrs`) have no confirmed votive code yet — see
"Lauds/Vespers duplex-majus+ Common-category psalmody" in the Open-work list above.
Regression tests added to both `tests/hours/lauds.test.ts` and `tests/hours/vespers.test.ts`:
a real category-tier date per hour (Our Lady of Mount Carmel/St. Benedict/Ss. Placid and
Companions for Lauds; St. Bartholomew/St. Lawrence's octave/St. Martin of Tours's First Vespers
for Vespers — several needed a different date than their existing office-bundle tests once the
actual governing evening was traced through `resolveEveningDay`/octave substitution), plus one
below-threshold (Semiduplex/Duplex) negative case per hour confirming the plain ferial default
still wins there. One pre-existing Vespers test (`2026-08-17`, "joins Ps 115 and 116... on
Monday") had to move to a different date: that Monday turned out to be the closing day of St.
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
existed. `npm test` (535 passed) and `npm run build` both pass.