Compare commits

..

5 Commits

Author SHA1 Message Date
will f7ff8fc0ae Fix Ezekiel spelling in the generated book-incipit heading
Deploy / deploy (push) Successful in 2m28s
The ezek incipit's English heading read "...Prophecy of Ezechiel" --
the Vulgate/Douay-Rheims spelling, inconsistent with this app's own
generated heading text elsewhere. Left every other in-corpus
"Ezechiel" alone (quoted Douay-Rheims verse/martyrology/homily prose,
where the traditional spelling is correct as source text).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 07:13:17 -04:00
will 3c8ccc3cbb Stop pooling the governing Sunday's own Matins content on a weekday
nocturnReadingIds pooled the governing Sunday's own nocturn-readings
id (temporalId) and its month-week-N sibling whenever any temporal
commemoration survived on a 3-nocturn day, with no weekday check --
so a Duplex+ saint winning outright on a plain weekday that also
carried a temporal commemoration pulled in a whole second homily's
worth of unrelated Sunday content. Live-verified against the
reference engine (Tridentine 1906, St. Joseph of Cupertino's day):
real Nocturn III there is just the winning saint's own Common homily
plus a single "Commemoratio Feriae" lesson -- no Sunday content at
all. Gates temporalKept to day.weekday === 'sunday'.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 07:13:07 -04:00
will 290bbac1b5 Route bible-plan incipit/responsory lookup through the book-alias table
bible-plan.ts's *.yml import uses English-style book abbreviations
("jdt", "1kgs", "luke", ...) for ~30 books where that diverges from
this store's canonical form ("jdth", "3reg", "luc", ...).
scripture/index.ts's own BOOK_ALIASES already normalized this for
verse-text lookup, but the incipit (bible-book-incipits.ts) and
responsory (matins-responsories.ts) lookups used the raw abbreviation
directly, silently missing on any canonically-keyed book and falling
back to a bare "Reading" heading. The 3 Gospel aliases self-healed via
a separate citation-regex fallback that only exists for Gospels; every
other aliased book (Judith, Kings, Samuel, Esdras, Osee, Jonas, Josue,
Micheas, Abdias, Judges, the Johannine epistles, James, Jude, Philemon,
Titus, Zacharias, Sophonias) had no such fallback. Exports
canonicalBook from scripture/index.ts and routes both lookups through
it -- one shared normalization instead of two independently-aliased
ones that can drift again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 07:12:55 -04:00
will 88e8a5b404 Name Ember days in the day label, demoted or winning outright
calendar/day-label.ts never surfaced an Ember day's own identity
anywhere: collectCommemorations only matched a temporal commemoration
against resolveTemporalId(day.date), never an Ember-relabeled id, and
the winning-outright fallback used the generic ordinal weekday label
with no id awareness at all. Adds a standalone emberDayLabel lookup,
deliberately kept out of temporal-feasts.ts since that table doubles
as isFerialOrVigil's bareness check and would have silently turned
off Ember days' own ferial Preces.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 07:12:32 -04:00
will 03d39dc911 Author Ember days' own Benedictus/Magnificat antiphons
Live-verified (Monastic Tridentinum 1617) that an Ember day's Lauds
psalmody stays on the plain ferial default throughout -- only the
Benedictus antiphon (and, Wednesday/Friday only, the Magnificat
antiphon) is genuinely proper to the day. Adds the 6 Benedictus +
4 Magnificat antiphon files (Ember Saturday's Second Vespers is
always ceded to the following Sunday on both quarters, so no
Magnificat content exists to author there).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 07:12:01 -04:00
18 changed files with 430 additions and 17 deletions
+122
View File
@@ -5192,3 +5192,125 @@ the old, incomplete assertion.
Full suite re-run after this follow-up: `npm test` (2021 passed, 47 files) and `npm run build` Full suite re-run after this follow-up: `npm test` (2021 passed, 47 files) and `npm run build`
(`tsc --noEmit` + `vite build`) both pass. (`tsc --noEmit` + `vite build`) both pass.
### Ember days' own Benedictus/Magnificat antiphons authored (2026-09-03)
Follow-on from the 2026-08-22 Ember-day mechanism entry, whose "not authored, logged instead"
note called the gap "the Ember-day-specific `[Ant 2]`/`[Ant 3]` Lauds antiphons" — imprecise, and
worth correcting here. Live-verified (Monastic Tridentinum 1617) against a clean year for each
Ember weekday (Wed 2023-09-20, Fri 2024-09-20, Sat 2025-09-20 for September; Dec 16/18/19 2026
for Advent, all winning outright): the day's own Lauds *psalmody* (Ps 50/63/64 + canticle +
Laudate, all 5 antiphon slots) stays entirely on the plain ferial weekday default, unchanged —
only the **Benedictus antiphon** (and, Wed/Fri only, the **Magnificat antiphon**) is genuinely
proper to the Ember day, each labeled "Antiphona ex Proprio de Tempore" live. `hours/resolve-
common.ts`'s `getBenedictusAntiphon`/`getMagnificatAntiphon` already look up `${winner.id}-
benedictus-antiphon`/`-magnificat-antiphon` unconditionally for any temporal winner (rank-
agnostic by design, same as the chapter/hymn bundle) — so this needed no mechanism change at
all, just the missing content files, which is why it silently rendered blank rather than
tripping any test.
**Authored**: 6 Benedictus antiphon files (one per Ember weekday × quarter) and 4 Magnificat
antiphon files (Wed + Fri only, both quarters). September Wednesday/Friday reuse the identical
text for both (`[Ant 3]` is a bare `@:Ant 2` cross-reference in the source, not independently
authored — a real fact about the source, not a shortcut taken here); Advent Wednesday's two
differ (`Missus est Gábriel` / `Ecce ancílla Dómini`); September Saturday uses the Monastic
track's own `TemporaM/093-6.txt` override (`Illumináre... Deus Israël`) per this project's
Monastic-1617-first preference, matching the choice already made for this day's Matins
responsories.
**Not authored, correctly**: Magnificat antiphons for either quarter's Ember Saturday — Second
Vespers on both is always ceded outright to the following Sunday ("Vespera de sequenti; nihil de
præcedenti," live-verified), and the source file itself has no `[Ant 3]` for either Saturday,
confirming the omission is real, not a gap. Advent Ember Friday's own Magnificat antiphon *was*
still authored despite being currently unreachable — Dec 18 always falls inside the O Antiphons'
own unconditional Dec 17-23 window, which wins the Vespers Magnificat outright — per this
project's standing "import available real content even if it may not survive the current
calendar" default.
New assertions added to `tests/calendar/ember-days.test.ts` (Benedictus antiphon text/status on
all 6 winning-outright dates; Magnificat status on Advent Ember Wednesday, the one case with no
overriding mechanism to complicate the assertion). `npm test` (2023 passed, 47 files) and
`tsc --noEmit` both pass.
**Follow-on, same day**: user asked why the Ember days didn't actually show anywhere in the app
on 2026-09-16/18/19 despite the above. Real bug, found by resolving `getDayLabel` directly for
those dates: `calendar/day-label.ts`'s `collectCommemorations` only ever matched a `temporal`-kind
commemoration against `resolveTemporalId(day.date)` (the plain, un-relabeled id for that date) —
never against an Ember-relabeled id, so `applyEmberDay`'s own commemoration was silently dropped
from the label every time. Worse, even the *winning-outright* case had no name: the final
fallback branch calls the generic `temporalLabel(day)` (pure season/weekday arithmetic, no id
awareness at all), so an Ember day that won outright rendered as bare "Wednesday in the Nth week
after Trinity (Feria)" with zero mention of Ember anywhere.
Fixed with a small standalone `EMBER_DAY_NAMES`/`emberDayLabel` lookup in `day-label.ts` itself
(6 entries, Latin names read straight from each day's own `[Officium]` header) — deliberately
*not* added to `temporal-feasts.ts`'s named-feast table, since that table doubles as `resolve-
common.ts`'s `isFerialOrVigil` bareness check (`isBareFeria = ... && !getTemporalFeastRecord(...)`)
and adding Ember ids there would have silently turned off their own ferial Preces (their `[Rule]`
is literally "Preces Feriales"). Wired into both places: `collectCommemorations`'s temporal
branch (demoted case) and `getDayLabel`'s final fallback (winning-outright case). New assertions
in `tests/calendar/ember-days.test.ts` cover both. `npm test` (2024 passed) and `tsc --noEmit`
both pass.
**Second follow-on, same session**: user then flagged `/vu/2026-09-18/matins` (Ember Friday,
commemorated) showing the Judith reading under a bare "Reading" heading. Real root cause, and a
much broader bug than just Judith: `data/hours/bible-plan/*.yml`'s ~390-row TSV import uses
English-style book abbreviations (`jdt`, `1kgs`, `luke`, ...) for the ~30 books where that
diverges from this store's canonical Vulgate-numbering form (`jdth`, `3reg`, `luc`, ...) —
`scripture/index.ts`'s own `BOOK_ALIASES` already normalizes this for verse-*text* lookup, but
`bible-plan.ts`'s incipit (`bible-book-incipits.ts`) and responsory (`matins-responsories.ts`)
lookups both used the raw, unaliased abbreviation, silently missing on any canonically-keyed
book and falling back to the generic "Reading" heading. The 3 Gospel aliases (`luke`/`mark`/
`john`) happened to self-heal through a second, independent citation-regex fallback that only
exists for Gospels (`matins.ts`'s `getGospelIncipitFromCitation`) — every other aliased book
(Judith, both Kings pairs, both Samuel books, Esdras, Osee, Jonas, Josue, Micheas, Abdias,
Judges, the 3 Johannine epistles, James, Jude, Philemon, Titus, Zacharias, Sophonias) had no such
fallback and was silently broken wherever it happened to be the first book of a reading. Fixed by
exporting `canonicalBook` from `scripture/index.ts` (the same substitution `getScriptureChapter`
already applies internally) and routing both `bible-plan.ts` lookups through it — one shared
normalization, not two independently-aliased lookups that can drift again. New test in
`tests/propers/bible-plan.test.ts`. `npm test` (2025 passed) and `tsc --noEmit` both pass.
Separately, the user also asked about seeing 3 lessons in a row all headed "Pope St. Gregory the
Great, Homily 33 on the Gospels, on Luke 7:36-50" (Ember Friday's own 3-lesson split of one
homily, each with its own distinct text/responsory — see the 2026-08-22 Ember-day entry above).
Confirmed this is correct content, not a duplication bug, but also confirmed it's *not* actually
how the reference source itself labels it: `Tempora/093-5.txt` only carries the attribution once,
on `[Lectio1]``[Lectio2]`/`[Lectio3]` have no repeated header at all, continuing silently under
the same nocturn. This app's own authoring convention repeats the full `source` string identically
on every split lesson, and does so consistently across the *entire* nocturn-readings corpus, not
just Ember days — flagged for the user as a real presentational rough edge, not fixed yet pending
their call on scope (a corpus-wide UI/authoring change, not a narrow one-file fix).
**Third follow-on, same session**: user then asked why a Sunday's own patristic homily was
showing up on a Friday at all. Pulled the live reference engine's own Matins page for St. Joseph
of Cupertino's day (Tridentine 1906) to check, rather than reasoning from vu's data alone: real
Nocturn III there is exactly 3 lessons — 2 from the winning Duplex saint's own Common homily, plus
a single "**Commemoratio Feriæ**" lesson for the displaced Ember day. No Sunday content
whatsoever. `hours/matins.ts`'s `nocturnReadingIds` had a `temporalKept` block that pooled the
governing Sunday's own id (`temporalId`) *and* its `month-week-N` sibling whenever any temporal
commemoration survived on a 3-nocturn day, with no weekday check at all — so a Duplex+ saint
winning outright on a plain weekday (not a Sunday) that also happened to carry a temporal
commemoration pulled in a whole second homily's worth of unrelated content (here: St. Ambrose's
homily + its own Gospel from `post-pentecost-16`, and St. Leo's September-fast sermon from
`month-week-093` — two different reference-engine representations of the *same* underlying
Sunday, both wrongly pooled at once). Fixed by gating `temporalKept` to `day.weekday === 'sunday'`
— the governing Sunday's own content only belongs in the pool when that Sunday is the day actually
being celebrated. Live-verified by reloading `/vu/2026-09-18/matins`: both spurious readings are
gone. `npm test` (2025 passed) and `tsc --noEmit` both pass.
Still open, by the user's own explicit request to land this first: Ember Friday's own homily
still surfaces as all 3 of its lessons even when merely commemorated, where the live engine caps
a commemorated feria at exactly one lesson (its own Lectio 1 only) — same shape of fix as this
one, not yet done.
### Ezekiel spelling fix in the book-incipit heading (2026-09-04)
User caught `bible-book-incipits.ts`'s `ezek` entry reading "A reading from the Prophecy of
**Ezechiel**" — the Vulgate/Douay-Rheims spelling, inconsistent with this app's own generated
heading text (which otherwise uses modern English book names, see that file's own header note on
"Saint Paul" vs "blessed Paul"). Changed to "Ezekiel". Left every other in-corpus "Ezechiel"
alone (scripture verse text, martyrology narrative, homily quotes under `src/data/scripture/`,
`src/data/martyrology/`, `src/data/propers/`) — those are quoted Douay-Rheims translation prose,
where the traditional spelling is correct as part of the source text, not this app's own
generated label.
+38 -1
View File
@@ -50,6 +50,38 @@ function joinBi(items: Bi[], sep = ' — '): Bi {
return { en: items.map((i) => i.en).join(sep), la: items.map((i) => i.la).join(sep) }; return { en: items.map((i) => i.en).join(sep), la: items.map((i) => i.la).join(sep) };
} }
/**
* The 6 Ember-day ids (calendar/ember-days.ts) are deliberately *not* in
* temporal-feasts.ts's named-feast table — they're privileged ferias, not
* rank-contesting movable feasts (see that file's own doc comment), and
* that table's `getTemporalFeastRecord` also doubles as resolve-
* common.ts's `isFerialOrVigil` bareness check, so adding them there
* would silently turn off their own ferial Preces (their `[Rule]` block
* is literally "Preces Feriales"). A small standalone lookup here instead
* — Latin names read straight from each day's own `[Officium]` header
* (Tempora/093-{3,5,6}.txt, Tempora/Adv3-{3,5,6}.txt).
*
* Without this, an Ember day never named itself anywhere in the UI: not
* as the primary label when it wins outright (fell through to the plain
* "Wednesday in the Nth week after Trinity"/"In the Nth week of Advent"
* wording with no Ember mention at all), and not as a commemoration when
* demoted (collectCommemorations's temporal branch only ever matched
* `resolveTemporalId(day.date)`, never an Ember-relabeled id) — live-
* verified as a real, user-visible gap, 2026-09-03.
*/
const EMBER_DAY_NAMES: Record<string, Bi> = {
'ember-september-wednesday': bi('Ember Wednesday in September', 'Feria Quarta Quattuor Temporum Septembris'),
'ember-september-friday': bi('Ember Friday in September', 'Feria Sexta Quattuor Temporum Septembris'),
'ember-september-saturday': bi('Ember Saturday in September', 'Sabbato Quattuor Temporum Septembris'),
'ember-advent-wednesday': bi('Ember Wednesday in Advent', 'Feria IV Quattuor Temporum in Adventu'),
'ember-advent-friday': bi('Ember Friday in Advent', 'Feria VI Quattuor Temporum in Adventu'),
'ember-advent-saturday': bi('Ember Saturday in Advent', 'Sabbato Quattuor Temporum in Adventu'),
};
function emberDayLabel(id: string): Bi | undefined {
return EMBER_DAY_NAMES[id];
}
function capitalize(text: string): string { function capitalize(text: string): string {
return text.charAt(0).toUpperCase() + text.slice(1); return text.charAt(0).toUpperCase() + text.slice(1);
} }
@@ -458,6 +490,11 @@ function collectCommemorations(
} else if (c.kind === 'temporal') { } else if (c.kind === 'temporal') {
if (c.id === temporalId) { if (c.id === temporalId) {
temporal.push(anchorDayName(day, false) ?? temporalLabel(day)); temporal.push(anchorDayName(day, false) ?? temporalLabel(day));
} else {
const emberName = emberDayLabel(c.id);
if (emberName) {
temporal.push(emberName);
}
} }
} else if (opts.showOctaves && c.id !== opts.excludeOctaveId) { } else if (opts.showOctaves && c.id !== opts.excludeOctaveId) {
const octave = activeOctaves.find((a) => a.id === c.id); const octave = activeOctaves.find((a) => a.id === c.id);
@@ -663,7 +700,7 @@ export function getDayLabel(day: LiturgicalDay): Partial<Record<string, string>>
} }
const temporalRank = TEMPORAL_CATEGORY_RANK_LABELS[day.temporalCategory]; const temporalRank = TEMPORAL_CATEGORY_RANK_LABELS[day.temporalCategory];
const label = temporalLabel(day); const label = (day.winner.kind === 'temporal' ? emberDayLabel(day.winner.id) : undefined) ?? temporalLabel(day);
const temporal = bi(`${label.en} (${temporalRank.en})`, `${label.la} (${temporalRank.la})`); const temporal = bi(`${label.en} (${temporalRank.en})`, `${label.la} (${temporalRank.la})`);
const { sanctoral } = collectCommemorations(day, { showOctaves: false }); const { sanctoral } = collectCommemorations(day, { showOctaves: false });
return withTransferNote(day, joinBi([temporal, ...sanctoral])); return withTransferNote(day, joinBi([temporal, ...sanctoral]));
@@ -0,0 +1,10 @@
# Verified against Divinum Officium (Monastic Tridentinum 1617), Lauds
# live query, 2026-12-18 -- Advent Ember Friday, labeled "Antiphona ex
# Proprio de Tempore". Same text as Tempora/Adv3-5.txt's own [Ant 2].
id: ember-advent-friday-benedictus-antiphon
text:
la: "Ex quo facta est * vox salutatiónis tuæ in áuribus meis, exsultávit in gáudio infans in útero meo, allelúja."
en: "As soon as the voice of thy salutation * sounded in mine ears, the babe leaped in my womb for joy. Alleluia."
status:
la: verified
en: verified
@@ -0,0 +1,17 @@
# Real content from Tempora/Adv3-5.txt's own [Ant 3], authored per this
# project's "import available content even if it may not survive the
# current calendar" default -- but currently unreachable in practice:
# Advent Ember Friday always falls on Dec 18 (the Wednesday after the 3rd
# Sunday of Advent is always Dec 17-23's own week), which the O Antiphons
# mechanism (calendar/index.ts, unconditional date-keyed override, see
# TODO.md 2026-08-30) always wins for the Vespers Magnificat antiphon
# instead -- live-verified 2026-12-18 renders "O Adonái," not this text.
# Kept for completeness/correctness of the id space, same as any other
# real proper text this store authors ahead of its own reachability.
id: ember-advent-friday-magnificat-antiphon
text:
la: "Hoc est testimónium, * quod perhíbuit Joánnes: Qui post me venit, ante me factus est."
en: "John bare witness of Him, * saying He That cometh after me is preferred before me."
status:
la: verified
en: verified
@@ -0,0 +1,18 @@
# Verified against Divinum Officium (Monastic Tridentinum 1617), Lauds
# live query, 2026-12-19 -- Advent Ember Saturday, labeled "Antiphona ex
# Proprio de Tempore". Same text as Tempora/Adv3-6.txt's own plain [Ant
# 2] (not the "(rubrica cisterciensis)" alternate below it -- no
# Cistercian-specific track modeled in this app).
#
# No Magnificat antiphon file for this id: Ember Saturday's own Second
# Vespers is always ceded to the 4th Sunday of Advent (live-verified
# 2026-12-19: "Dominica IV Adventus"), same "Vespera de sequenti" pattern
# already confirmed for September's own Ember Saturday -- the source file
# itself has no [Ant 3] for this day either.
id: ember-advent-saturday-benedictus-antiphon
text:
la: "Quómodo fiet istud, * Angele Dei, quóniam virum non cognósco? Audi, María Virgo: Spíritus Sanctus supervéniet in te, et virtus Altíssimi obumbrábit tibi."
en: "O thou Angel of God, * how shall this be, seeing I know not a man? Hear, O Virgin Mary! the Holy Ghost shall come upon thee, and the power of the Highest shall overshadow thee!"
status:
la: verified
en: verified
@@ -0,0 +1,12 @@
# Verified against Divinum Officium (Monastic Tridentinum 1617), Lauds
# live query, 2026-12-16 -- Advent Ember Wednesday, labeled "Antiphona ex
# Proprio de Tempore". Same text as Tempora/Adv3-3.txt's own [Ant 2].
# Lauds psalmody itself stays on the plain ferial Wednesday antiphons
# throughout (confirmed live), same as September's own Ember Wednesday.
id: ember-advent-wednesday-benedictus-antiphon
text:
la: "Missus est Gábriel * Angelus ad Maríam Vírginem desponsátam Joseph."
en: "The angel Gabriel * was sent to Mary, a virgin espoused to Joseph."
status:
la: verified
en: verified
@@ -0,0 +1,14 @@
# Verified against Divinum Officium (Monastic Tridentinum 1617), Vespers
# live query, 2026-12-16 -- Advent Ember Wednesday's own Second Vespers,
# labeled "Antiphona ex Proprio de Tempore" (Dec 16 falls just before the
# O Antiphons' own Dec 17-23 window, so this really is what's said, unlike
# Friday's own sibling file below). Distinct text from the Benedictus
# antiphon this time (Tempora/Adv3-3.txt's own separate [Ant 3]), not a
# reused cross-reference like September's Wednesday/Friday.
id: ember-advent-wednesday-magnificat-antiphon
text:
la: "Ecce ancílla Dómini: * fiat mihi secúndum verbum tuum."
en: "Behold the handmaid * of the Lord; be it unto me according to thy word."
status:
la: verified
en: verified
@@ -0,0 +1,15 @@
# Verified against Divinum Officium (Monastic Tridentinum 1617), Lauds
# live query, 2024-09-20 -- the clean year in range where September Ember
# Friday itself wins outright, labeled "Antiphona ex Proprio de Tempore".
# Same text as Tempora/093-5.txt's own [Ant 2] (the plain secular
# recension, not the "(rubrica cisterciensis)" alternate below it -- no
# Cistercian-specific track modeled in this app). Lauds psalmody itself
# stays on the plain ferial Friday antiphons throughout, same as
# Wednesday's own sibling file.
id: ember-september-friday-benedictus-antiphon
text:
la: "Múlier, * quæ erat in civitáte peccátrix, stans retro secus pedes Dómini, lácrimis cœpit rigáre pedes ejus et capíllis cápitis sui tergébat, et deosculabátur pedes ejus et unguénto ungébat."
en: "A woman in the city which was a sinner stood at the Lord's Feet behind Him, * and began to wash His Feet with tears, and did wipe them with the hairs of her head, and kissed His Feet, and anointed them with the ointment."
status:
la: verified
en: verified
@@ -0,0 +1,10 @@
# Tempora/093-5.txt's [Ant 3] "(rubrica 196)" is a bare "@:Ant 2"
# cross-reference, same identical text reused for Vespers as the sibling
# -benedictus-antiphon.yml -- see that file's own comment.
id: ember-september-friday-magnificat-antiphon
text:
la: "Múlier, * quæ erat in civitáte peccátrix, stans retro secus pedes Dómini, lácrimis cœpit rigáre pedes ejus et capíllis cápitis sui tergébat, et deosculabátur pedes ejus et unguénto ungébat."
en: "A woman in the city which was a sinner stood at the Lord's Feet behind Him, * and began to wash His Feet with tears, and did wipe them with the hairs of her head, and kissed His Feet, and anointed them with the ointment."
status:
la: verified
en: verified
@@ -0,0 +1,24 @@
# Verified against Divinum Officium (Monastic Tridentinum 1617), Lauds
# live query, 2025-09-20 -- the clean year in range where September Ember
# Saturday itself wins outright, labeled "Antiphona ex Proprio de
# Tempore". Uses the Monastic track's own override text
# (Latin/TemporaM/093-6.txt's [Ant 2], "Illumináre... Deus Israël") in
# place of the secular Tempora/093-6.txt's plain "Illúmina... in viam
# pacis" (no "Deus Israël"), per this project's Monastic-1617-first
# structure preference -- same choice already made for this day's Matins
# responsories. The English/Tempora/093-6.txt translation already carries
# "O Thou the God of Israel," confirming it was translated from the
# Monastic Latin, not the secular.
#
# No Magnificat antiphon file for this id: Ember Saturday's own Second
# Vespers is always ceded outright to the following Sunday ("Vespera de
# sequenti; nihil de præcedenti," live-verified 2025-09-20) -- the source
# file itself has no [Ant 3] for this day, correctly reflecting that the
# text would never be said.
id: ember-september-saturday-benedictus-antiphon
text:
la: "Illumináre, Dómine, * his, qui in ténebris sedent: et dírige pedes nostros in viam pacis, Deus Israël."
en: "Give light, O Lord, * to them that sit in darkness, and guide our feet into the way of peace, O Thou the God of Israel."
status:
la: verified
en: verified
@@ -0,0 +1,17 @@
# Verified against Divinum Officium (Monastic Tridentinum 1617), Lauds
# live query, 2023-09-20 -- the one clean year in range where September
# Ember Wednesday itself wins outright (no competing saint), labeled
# "Antiphona ex Proprio de Tempore". Same text as Tempora/093-3.txt's own
# [Ant 2] ("Hoc genus...", echoing Lectio3's Gospel line): the app's
# Lauds psalmody itself stays entirely on the plain ferial Wednesday
# antiphons (Ps 50/63/64 + canticle + Laudate all confirmed unchanged
# live) -- only the Benedictus antiphon is genuinely proper to the Ember
# day, not the whole psalm-antiphon block TODO.md's 2026-08-22 entry
# loosely called "Lauds antiphons."
id: ember-september-wednesday-benedictus-antiphon
text:
la: "Hoc genus * dæmoniórum in nullo potest exíre, nisi in oratióne et jejúnio."
en: "This kind of devils * can come forth by nothing but by prayer and fasting."
status:
la: verified
en: verified
@@ -0,0 +1,12 @@
# Same source as the sibling -benedictus-antiphon.yml: Tempora/093-3.txt's
# [Ant 3] is a bare "@:Ant 2" cross-reference, i.e. the identical text
# reused for both Lauds and Vespers on this day -- not independently
# authored content, but the reference engine's own real choice, not a
# simplification on this app's part.
id: ember-september-wednesday-magnificat-antiphon
text:
la: "Hoc genus * dæmoniórum in nullo potest exíre, nisi in oratióne et jejúnio."
en: "This kind of devils * can come forth by nothing but by prayer and fasting."
status:
la: verified
en: verified
+30 -13
View File
@@ -654,20 +654,37 @@ function nocturnReadingIds(day: LiturgicalDay, temporalId: string, date: string,
} }
// The plain temporalId/month-week content is the *governing Sunday's own* // The plain temporalId/month-week content is the *governing Sunday's own*
// Nocturn 2/3 patristic material, real content for that Sunday itself — // Nocturn 2/3 patristic material, real content for that Sunday itself —
// only pooled on a real 3-nocturn day, and (as before) only when the // only pooled on a real 3-nocturn day, only on the Sunday itself (see
// day's own occurrence decision (calendar/commemorations.ts's // below), and (as before) only when the day's own occurrence decision
// decideOccurrence) actually retained the temporal identity in some form: // (calendar/commemorations.ts's decideOccurrence) actually retained the
// the temporal cycle won outright (day.winner.kind === 'temporal' -- a // temporal identity in some form: the temporal cycle won outright
// plain Sunday, or a named temporal override like Christ the King), or it // (day.winner.kind === 'temporal' -- a plain Sunday, or a named temporal
// survives as a commemoration alongside a sanctoral winner. Excluded: // override like Christ the King), or it survives as a commemoration
// decideOccurrence's `ordinary-feria` branch, where a real feast -- // alongside a sanctoral winner. Excluded: decideOccurrence's
// however low-ranked -- wins with zero commemorations, correctly // `ordinary-feria` branch, where a real feast -- however low-ranked --
// suppressing the temporal identity entirely (e.g. St. Bartholomew, // wins with zero commemorations, correctly suppressing the temporal
// duplex-2-classis, 2026-08-24 -- his own proper reading has no Nocturn // identity entirely (e.g. St. Bartholomew, duplex-2-classis, 2026-08-24
// 3 content, and without this gate the leftover 13th-Sunday-after- // -- his own proper reading has no Nocturn 3 content, and without this
// Pentecost/month-week content wrongly filled Nocturn 3 instead). // gate the leftover 13th-Sunday-after-Pentecost/month-week content
// wrongly filled Nocturn 3 instead).
//
// `day.weekday === 'sunday'` gate added 2026-09-03: live-verified
// (Tridentine 1906) that a Duplex+ saint winning outright on a *weekday*
// that also happens to carry a temporal commemoration (e.g. St. Joseph
// of Cupertino, Duplex, Friday Sept 18) gets no Sunday content at all --
// the reference engine's own Nocturn III there is just the saint's own
// Common homily (2 lessons) plus a single "Commemoratio Feriæ" lesson
// for the displaced feria, never the week's separate governing-Sunday
// homily (Ambrose's/Leo's, in that live case). This mechanism previously
// fired on *any* 3-nocturn day with a surviving temporal commemoration,
// regardless of weekday, wrongly pooling in a whole extra Sunday's worth
// of unrelated patristic content on top of the day's own two real
// sources. The governing Sunday's own content only belongs in the pool
// when that Sunday *is* the day being celebrated.
const temporalKept = const temporalKept =
threeNocturns && (day.winner.kind === 'temporal' || day.commemorations.some((c) => c.kind === 'temporal')); threeNocturns &&
day.weekday === 'sunday' &&
(day.winner.kind === 'temporal' || day.commemorations.some((c) => c.kind === 'temporal'));
if (temporalKept) { if (temporalKept) {
ids.add(temporalId); ids.add(temporalId);
const monthWeek = monthWeekId(date); const monthWeek = monthWeekId(date);
+1 -1
View File
@@ -58,7 +58,7 @@ export const BOOK_INCIPITS: Record<string, { la: string; en: string }> = {
jer: { la: 'Léctio Jeremíæ Prophétæ', en: 'A reading from the Prophecy of Jeremias' }, jer: { la: 'Léctio Jeremíæ Prophétæ', en: 'A reading from the Prophecy of Jeremias' },
lam: { la: 'Léctio Threnórum Jeremíæ Prophétæ', en: 'A reading from the Lamentations of Jeremias the Prophet' }, lam: { la: 'Léctio Threnórum Jeremíæ Prophétæ', en: 'A reading from the Lamentations of Jeremias the Prophet' },
bar: { la: 'Léctio Baruch Prophétæ', en: 'A reading from the Prophecy of Baruch' }, bar: { la: 'Léctio Baruch Prophétæ', en: 'A reading from the Prophecy of Baruch' },
ezek: { la: 'Léctio Ezechiélis Prophétæ', en: 'A reading from the Prophecy of Ezechiel' }, ezek: { la: 'Léctio Ezechiélis Prophétæ', en: 'A reading from the Prophecy of Ezekiel' },
dan: { la: 'Léctio Daniélis Prophétæ', en: 'A reading from the Prophecy of Daniel' }, dan: { la: 'Léctio Daniélis Prophétæ', en: 'A reading from the Prophecy of Daniel' },
// Minor prophets // Minor prophets
osee: { la: 'Léctio Osee Prophétæ', en: 'A reading from the Prophecy of Osee' }, osee: { la: 'Léctio Osee Prophétæ', en: 'A reading from the Prophecy of Osee' },
+4 -2
View File
@@ -21,6 +21,7 @@ import type { LanguageCode, TranslationStatus } from '../psalter/types';
import { resolvePassages, type ScriptureCitation } from './octave-readings'; import { resolvePassages, type ScriptureCitation } from './octave-readings';
import { getResponsoryForBook } from './matins-responsories'; import { getResponsoryForBook } from './matins-responsories';
import { getBookIncipit } from './bible-book-incipits'; import { getBookIncipit } from './bible-book-incipits';
import { canonicalBook } from '../scripture';
const GOSPEL_BOOKS = new Set(['matt', 'mark', 'luke', 'john']); const GOSPEL_BOOKS = new Set(['matt', 'mark', 'luke', 'john']);
@@ -115,8 +116,9 @@ function resolveReading(record: BiblePlanReadingRecord, bookIndex: number): Bibl
}; };
const citationLabelText = formatCitationLabel(record.passages); const citationLabelText = formatCitationLabel(record.passages);
const firstBook = record.passages[0]?.book; const firstBook = record.passages[0]?.book;
const responsory = firstBook ? getResponsoryForBook(firstBook, bookIndex)?.text : undefined; const canonicalFirstBook = firstBook ? canonicalBook(firstBook) : undefined;
const incipit = firstBook ? getBookIncipit(firstBook) : undefined; const responsory = canonicalFirstBook ? getResponsoryForBook(canonicalFirstBook, bookIndex)?.text : undefined;
const incipit = canonicalFirstBook ? getBookIncipit(canonicalFirstBook) : undefined;
return { return {
text, text,
status, status,
+13
View File
@@ -58,6 +58,19 @@ export function getScriptureChapter(book: string, chapter: number): ScriptureCha
return chapters.get(`${BOOK_ALIASES[book] ?? book}-${chapter}`); return chapters.get(`${BOOK_ALIASES[book] ?? book}-${chapter}`);
} }
/** `book` normalized to this store's own canonical abbreviation — the
* same substitution `getScriptureChapter` applies internally, exposed for
* callers (bible-plan.ts's incipit/responsory lookups) that key off a
* *different* store keyed by the canonical form only (bible-book-
* incipits.ts, matins-responsories.ts) and would otherwise silently miss
* on any of `BOOK_ALIASES`' ~30 entries — live case: "jdt" (Judith)
* resolves its verse text fine but fell through bible-book-incipits.ts's
* `jdth`-keyed lookup, rendering as a bare "Reading" heading instead of
* "A reading from the book of Judith" (2026-09-03). */
export function canonicalBook(book: string): string {
return BOOK_ALIASES[book] ?? book;
}
/** /**
* @param verseRange e.g. "2-19" — inclusive. Whole (authored) chapter when * @param verseRange e.g. "2-19" — inclusive. Whole (authored) chapter when
* omitted. Unauthored book/chapter combinations resolve to an empty array * omitted. Unauthored book/chapter combinations resolve to an empty array
+52
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { resolveDay } from '../../src/calendar'; import { resolveDay } from '../../src/calendar';
import { getDayLabel } from '../../src/calendar/day-label';
import { resolveOrdo } from '../../src/hours'; import { resolveOrdo } from '../../src/hours';
import { getDayCollect } from '../../src/hours/resolve-common'; import { getDayCollect } from '../../src/hours/resolve-common';
@@ -53,6 +54,57 @@ describe('Ember days', () => {
} }
}); });
it("Lauds' Benedictus antiphon is the Ember day's own proper text on all 6 winning-outright days (2026-08-22 added the mechanism/collect/Matins, but not this -- fixed here)", () => {
// Wed/Fri/Sat winning outright for the September quarter, one per
// weekday (2026's own dates are all saint-outranked, see above) --
// found by scanning forward from 2023, all live-verified against the
// reference engine.
const cases: [string, string][] = [
['2023-09-20', 'Hoc genus'],
['2024-09-20', 'Múlier'],
['2025-09-20', 'Illumináre, Dómine'],
['2026-12-16', 'Missus est Gábriel'],
['2026-12-18', 'Ex quo facta est'],
['2026-12-19', 'Quómodo fiet istud'],
];
for (const [date, expectedStart] of cases) {
const ordo = resolveOrdo('lauds', date);
const idx = ordo.parts.findIndex((p) => p.kind === 'canticle' && p.canticleId === 'benedictus');
const antiphon = ordo.parts[idx + 1] as { text: { text: { la?: string }; status: { la?: string; en?: string } } };
expect(antiphon.text.status.la).toBe('verified');
expect(antiphon.text.status.en).toBe('verified');
expect(antiphon.text.text.la).toContain(expectedStart);
}
});
it("Vespers' Magnificat antiphon is proper on Ember Wednesday (Sept and Advent) where the reference engine actually renders it, not overridden by a Sunday/O-Antiphon", () => {
// September Ember Friday/Saturday and Advent Ember Friday/Saturday
// are excluded here: Saturday's Second Vespers is always ceded to the
// following Sunday (live-verified), and Advent Ember Friday/Saturday
// (always Dec 18/19) are always inside the O Antiphons' own
// unconditional Dec 17-23 window -- both real, not gaps.
const ordo = resolveOrdo('vespers', '2026-12-16');
const idx = ordo.parts.findIndex((p) => p.kind === 'canticle' && p.canticleId === 'magnificat');
const antiphon = ordo.parts[idx + 1] as { text: { status: { la?: string; en?: string } } };
expect(antiphon.text.status.la).toBe('verified');
expect(antiphon.text.status.en).toBe('verified');
});
it("the day label names the Ember day itself, both when it's demoted to a commemoration and when it wins outright -- previously never shown anywhere in the UI (2026-09-03 fix)", () => {
// Demoted cases (2026): a saint wins outright, Ember rides along as a
// commemoration -- collectCommemorations' temporal branch previously
// only matched resolveTemporalId(day.date), never an Ember-relabeled id.
expect(getDayLabel(resolveDay('2026-09-16')).en).toContain('Ember Wednesday in September');
expect(getDayLabel(resolveDay('2026-09-18')).en).toContain('Ember Friday in September');
expect(getDayLabel(resolveDay('2026-09-19')).en).toContain('Ember Saturday in September');
// Winning-outright cases: previously fell through to the plain
// ordinal weekday label with no Ember mention at all.
expect(getDayLabel(resolveDay('2023-09-20')).en).toBe('Ember Wednesday in September (Feria) — Vigil of St. Matthew');
expect(getDayLabel(resolveDay('2026-12-16')).en).toBe('Ember Wednesday in Advent (Feria)');
expect(getDayLabel(resolveDay('2026-12-18')).en).toBe('Ember Friday in Advent (Feria)');
expect(getDayLabel(resolveDay('2026-12-19')).en).toContain('Ember Saturday in Advent (Feria)');
});
it('Matins renders 3 real, distinct, verified lessons+responsories for each of the 6 Ember days', () => { it('Matins renders 3 real, distinct, verified lessons+responsories for each of the 6 Ember days', () => {
const dates = ['2026-09-16', '2026-09-18', '2026-09-19', '2026-12-16', '2026-12-18', '2026-12-19']; const dates = ['2026-09-16', '2026-09-18', '2026-09-19', '2026-12-16', '2026-12-18', '2026-12-19'];
for (const date of dates) { for (const date of dates) {
+21
View File
@@ -17,3 +17,24 @@ describe('getBiblePlanReadings citation labels', () => {
expect(readings[2]?.citation.la).toBe('Luke 18:1-23'); expect(readings[2]?.citation.la).toBe('Luke 18:1-23');
}); });
}); });
// A reading's `label` (the "Léctio libri..." incipit) previously looked up
// bible-book-incipits.ts's canonical-abbreviation-only table using the raw
// book abbreviation from data/hours/bible-plan/*.yml -- which, for ~30
// books, is one of scripture/index.ts's own English-style aliases (e.g.
// "jdt" for Judith), not the canonical form ("jdth") that table is keyed
// by. The alias resolved verse *text* fine (getScriptureChapter already
// consulted the alias table) but silently missed the label lookup,
// rendering as the generic "Reading" heading instead -- live case: Sept
// 18 2026's own Ember Friday commemoration, `post-pentecost-16-friday`'s
// first reading (Judith 11-13). Fixed 2026-09-03 by routing both the
// incipit and responsory lookups through scripture/index.ts's own
// `canonicalBook`, the same normalization the text lookup already used.
describe('getBiblePlanReadings label uses the same book-alias normalization as verse-text lookup', () => {
it('an aliased book ("jdt" for Judith) still gets its real incipit label, not the generic fallback', () => {
const readings = getBiblePlanReadings('post-pentecost-16', 'friday', '2026-09-18');
expect(readings[0]?.citation.la).toBe('Jdt 11-13');
expect(readings[0]?.label?.en).toBe('A reading from the book of Judith');
expect(readings[0]?.label?.la).toBe('Léctio libri Judith');
});
});