Compare commits

...

202 Commits

Author SHA1 Message Date
will f8bd7f3140 Dedupe Matins nocturn readings pooled from two ids on the same day
Deploy / deploy (push) Successful in 2m12s
post-pentecost-15 and month-week-092 land on the same Sunday
(2026-09-06) and independently reuse the identical Gregory the Great
Moralia in Job excerpt -- a real duplication in the reference engine's
own Tempora files, not a vu authoring error. buildReadingPool pooled
both ids and rendered the reading twice; now dedupes by text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKC2QQGLYo2UTiDjDyWEkW
2026-09-05 07:22:53 -04:00
will 6ecd1485c4 Add jdc (Judges) responsory, composed from Gideon's cry at Jdc 7:17-20
Deploy / deploy (push) Successful in 1m52s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKC2QQGLYo2UTiDjDyWEkW
2026-09-05 07:14:07 -04:00
will 9e66ad10e4 Add josue (Joshua) responsory, composed from Josue 24:15-16
Deploy / deploy (push) Successful in 1m56s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKC2QQGLYo2UTiDjDyWEkW
2026-09-05 07:04:25 -04:00
will dcea19015f Add 1esdr (Ezra) responsory, composed from 1 Esdr 8:21-23
Deploy / deploy (push) Successful in 1m56s
No liturgical precedent exists for this book; the verse's antithesis
("the hand of our God is upon all them that seek him in goodness: and
his power and strength, and wrath upon all them that forsake him") is
reordered so the hopeful clause repeats after the V., not the wrath
clause. Closes the per-book pool at 68/72 -- the last 4 have no
sourceable material at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ALE7phKTAnmRWvWMU9dZa7
2026-09-05 06:39:18 -04:00
will c98abaebb9 Drop parenthetical gloss from Nehemiah responsory V.
Deploy / deploy (push) Successful in 2m12s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ALE7phKTAnmRWvWMU9dZa7
2026-09-05 06:24:21 -04:00
will 1d566ec6e0 Add 2esdr (Nehemiah), correcting a prior mis-diagnosis
Deploy / deploy (push) Successful in 2m2s
Nehemiah was wrongly bundled in with ezra's "ambiguous, not acted on"
reasoning and left unseeded. It actually has a real, unambiguous
citation in the Missa (Ember Wednesday of September's Lectio/Communio,
Neh 8:1-10) that a prior pass never checked. R. from v.3, V. from v.9,
both from that accented Missa Lectio.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ALE7phKTAnmRWvWMU9dZa7
2026-09-05 06:14:20 -04:00
will c2a15d94dd Add ruth -- composed with no liturgical precedent at all
Deploy / deploy (push) Successful in 1m51s
User-directed: compose Ruth's own responsory from Ruth 1:16-17 ("thy
people shall be my people, and thy God my God") on the verse's own
merit. Unlike deut/rom/bar, this text has no liturgical precedent
anywhere in the reference source (no Nuptial Mass file even exists
there) -- flagged that distinction before proceeding; user confirmed
explicitly.

Since data/scripture/ruth-1.yml (the usual bulk import) carries no
accents, the Latin was hand-accented rather than transcribed, but
cross-checked word-by-word against already-accented occurrences of
the same words elsewhere in the corpus rather than guessed freehand.

66 of 72 bible-plan books now seeded. npm test (2029 passed) and
tsc --noEmit pass; live-verified through getBiblePlanReadings.
Canonical-key audit re-run, clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 22:01:15 -04:00
will 3ee87098c0 Note user's explanation for the Ezra Introit citation mismatch
Deploy / deploy (push) Successful in 1m53s
Vulgate "Esdras" numbering is genuinely ambiguous: 1/2 Esdras is
canonical Ezra/Nehemiah, but there's also a separate non-canonical
Greek "Esdras A" (the Vulgate appendix's own 3 Esdras, sometimes
itself called "1 Esdras" in Septuagint/Orthodox numbering) -- a
different text this project has never digitized. The reference
engine's own citation never disambiguates, so this can't be
confirmed, but it cleanly explains why the Pasc7-2.txt Introit's
"Esdr 2:36-37" citation doesn't match our canonical Ezra text.
Practical outcome unchanged (ezra stays unseeded), now documented
with a clear reason rather than an open mystery.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 21:51:06 -04:00
will e52c285f99 Add bar (Baruch), found in the Missa propers, not the Horas
Deploy / deploy (push) Successful in 1m54s
User pointed to missa/Latin/Tempora/Quad6-6.txt -- the Mass propers, a
directory tree this whole responsory search had never once checked
(everything so far searched horas/, the Divine Office proper). The
Easter Vigil Mass's own fourth prophecy ([Proph_Baruch3], Bar 3:9-38,
"Audi, Israel, mandata vitae") is genuine, real, accented liturgical
Latin -- just never part of Matins at all.

Composed bar the same way as deut/rom: built from that prophecy's own
climactic close ("Hic est Deus noster... Post haec in terris visus
est, et cum hominibus conversatus est" -- read at the Vigil because
early exegetes saw "seen upon earth, and conversed with men" as a
prophecy of the Incarnation).

Extended the Missa check to the other 6 already-absent books too --
genuinely zero citations anywhere. Ezra turned up one thin, ambiguous
Missa Introit citation that doesn't match its own cited verse; left
unseeded rather than force it.

65 of 72 bible-plan books now seeded -- this closes the per-book
responsory pool. npm test (2029 passed) and tsc --noEmit pass;
live-verified through getBiblePlanReadings. Canonical-key audit
re-run, clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 21:36:48 -04:00
will 2b7495fc91 Close the Matins per-book responsory pool: rom is the last book
Deploy / deploy (push) Successful in 1m53s
Composed rom from Rom 13:11-12 ("the night is past, the day is at
hand... let us cast off the works of darkness, and put on the armour
of light") -- the standing year-round ferial Lauds Capitulum, also
used at Advent I's own Capitulum. Genuine accented Vulgate/
Douay-Rheims text, never historically assembled into its own Matins
responsory, same approach as deut. User-confirmed before adding.

64 of 72 bible-plan books now seeded -- every book actually used in
the bible-plan has a real responsory, whether transcribed (62) or
composed from genuine already-liturgical text (deut, rom). The
remaining 8 have no citation anywhere in the source at all; there's
nothing further to check.

npm test (2029 passed) and tsc --noEmit pass; live-verified through
getBiblePlanReadings. Canonical-key audit re-run, clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 21:14:03 -04:00
will afa187032b Add deut, this pool's first genuine original composition
Deploy / deploy (push) Successful in 1m55s
User found a lead reading a patristic homily (English/Sancti/01-02.txt)
that quotes Deut 32:11's eagle image. Deuteronomy has no historical
Matins responsory anywhere in the reference source, so unlike every
other entry in this pool, this one is composed rather than
transcribed: a real R./V. pairing built from the Canticle of Moses'
own genuine verses (Latin/Psalterium/Psalmorum/Psalm226.txt, sung at
Lauds), verified against data/scripture/deut-32.yml to confirm the
citation is standard verse 11 and not the breviary's own internal
chant-verse-numbering (32:15-16) for the same text. Documented plainly
in the data file's own header as vu's composition, not presented as
historically attested.

63 of 72 bible-plan books now seeded. npm test (2029 passed) and
tsc --noEmit pass; live-verified through getBiblePlanReadings.
Canonical-key audit re-run, clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 20:06:48 -04:00
will d39359e5ac Carry over unreached Advent bible-plan readings into Christmastide
Deploy / deploy (push) Successful in 1m55s
Advent's own reading plan (advent-1-sunday through advent-4-saturday,
28 rows for a full 4-week Advent) is always longer than real Advent
runs. Advent's last real day is always Dec 23 (Dec 24 is the Vigil of
Christmas's own temporal id), while Advent 1 Sunday lands anywhere
from Nov 27 to Dec 3 -- even the longest possible Advent is one row
short of the full plan, and the shortest is a whole week short. Every
year, at least the plan's last row never reaches its own natural date.

Added adventCarryoverReadings to propers/bible-plan.ts: whatever
doesn't land on its own natural date now carries forward one reading
per day starting Christmas Day, pooled alongside whatever
Christmastide's own calendar-dated rows already have -- never
replacing them. This is the one place in bible-plan.ts that needs
real calendar-year awareness (calendar/temporal.ts's adventStart).

4 new tests covering the shortest (2028) and a long (2027) Advent,
pooling behavior, and the Dec-24 boundary. npm test (2029 passed) and
tsc --noEmit pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 19:17:24 -04:00
will 52052d737e Find Canticles, Leviticus, and Numbers in Sancti, not Tempora
Deploy / deploy (push) Successful in 1m51s
User pushback on Canticles being "confirmed absent" -- right instinct
to check the Commune files, which redirected productively: the same
Lectio-citation survey run against Sancti (individual saint/feast
files) rather than just Tempora found real, genuine content:

- cant: the Visitation (July 2) reads straight through the Song of
  Songs, blending it with the Visitation narrative itself.
- lev: the Purification of the BVM's own Leviticus purification-law
  reading is the feast's actual reason for being.
- num: the Exaltation of the Holy Cross's bronze-serpent/Cross
  typology (Num 21:6-9).

Re-checked the other 9 already-"confirmed absent" books against
Sancti too -- still genuinely absent, no alternate abbreviation
missed.

62 of 72 bible-plan books now seeded -- the real final count. Only
Romans and 9 books with no citation anywhere (Tempora or Sancti)
remain. npm test (2025 passed) and tsc --noEmit pass; live-verified
through getBiblePlanReadings. Canonical-key audit re-run, clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 19:03:01 -04:00
will e96d201346 Close out the Matins per-book responsory pool: 59 of 72 books
Deploy / deploy (push) Successful in 1m49s
Asked to translate the 15 remaining "real Latin, no English" books.
None needed translation: the whole post-Epiphany reading season
(Galatians/Ephesians/Philippians/Colossians/1-2 Timothy/Titus/
Philemon/2 Thessalonians) turns out to have no distinct responsory
content of its own -- every feria file references 2 Corinthians' own
week (already bilingual) by weekday position. Same for 2 Peter/2-3
John/Jude, which reference Ascension Day's own responsories (act).

Only Romans genuinely has nothing: every citing file borrows an
unrelated feast's own proper responsory (Circumcision, Christmas Day,
a January saint), never anything Romans-specific. Left unseeded --
using one would repeat the Sacred Heart/Jeremiah mismatch this pool
already avoids elsewhere.

59 of 72 books now seeded -- every book with any usable Latin content
in the source at all. The remaining 13 (Romans plus 12 books with no
citation anywhere) are the real, final gap.

Ran the full canonical-key audit again; confirmed clean. npm test
(2025 passed) and tsc --noEmit pass; live-verified through
getBiblePlanReadings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 18:50:56 -04:00
will 1867f73080 Add R./V./repeat labels to per-book responsory pool output
Deploy / deploy (push) Successful in 1m54s
getResponsoryForBook's joinResponsory concatenated the r/v text with a
bare newline and no "R."/"V." labels at all, unlike every other
responsory source in the app (octave-readings' own responsory field,
nocturn-readings' inline responsory, and the new per-Common-category
pool) which all include the full "R. ...\n* ...\nV. ...\nR. ..."
convention. User caught this looking at the rendered Judith entry.

Fixed by computing the repeat line from the existing "*" marker
already present in the pool's r text. npm test (2025 passed) and
tsc --noEmit pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 18:44:38 -04:00
will ff87aff240 Add 1 Cor, 2 Cor, 1 Peter, 1 John; fix a repeated canonical-key bug
Deploy / deploy (push) Successful in 1m52s
Confirmed Leviticus/Numbers/Deuteronomy genuinely absent from Tempora
(precise awk-based survey, not a loose grep). Added 1cor (Maundy
Thursday's own Institution-narrative Nocturn 3), 2cor (Epiphany II
Sunday), 1pet and 1joan (Paschaltide Sunday sets already partly used
for jac/jas).

Caught a repeat of the same canonical-key mistake from the previous
commit: the jas entry should have been keyed jac (BOOK_ALIASES maps
jas->jac). Fixed, and this time audited every single key in the file
programmatically against BOOK_ALIASES rather than just the keys
touched this pass -- confirmed clean.

45 of 72 books now seeded. npm test (2025 passed) and tsc --noEmit
pass; live-verified through getBiblePlanReadings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 18:31:13 -04:00
will c32fabb54d Add Acts, Apocalypse, James, Hebrews, and the 4 Gospels to responsory pool
Deploy / deploy (push) Successful in 1m48s
Surveyed Tempora precisely with an awk one-liner reading the line after
every [LectioN] tag (two lines deep for Gospel citations) rather than a
loose grep-for-book-prefix, which had already produced one false
positive this session.

Added act (Ascension Day's own Matins), marc (Easter Sunday's own),
heb (Good Friday's own Tenebrae), apoc (Easter III Sunday), jas
(Easter IV Sunday), and luc/matt/joan (Advent I/II/III Sundays, one
entry each). 41 of 72 bible-plan books now seeded.

Confirmed no English exists anywhere in the reference engine for
Galatians/Ephesians/Philippians/Colossians/Titus/Philemon or Romans --
real Latin exists, no bilingual source; left unseeded.

npm test (2025 passed) and tsc --noEmit pass; live-verified through
the real getBiblePlanReadings call chain (not a direct
getResponsoryForBook call, per the previous commit's lesson).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 18:20:44 -04:00
will f0b771a02a Fix real correctness bug: pool keys must match canonical book form
Deploy / deploy (push) Successful in 1m50s
bible-plan.ts normalizes a reading's book through canonicalBook()
(scripture/index.ts's BOOK_ALIASES map) before calling
getResponsoryForBook -- so the pool must be keyed by the canonical
form, not the bible-plan YAML's own raw book code. 12 of the 33 keys
seeded earlier today used the raw form and would have silently
resolved to undefined in the real app despite passing an initial
"live verification" that called getResponsoryForBook directly
(bypassing the normalization the real call site always applies).

Renamed: 1macc->1mach, 2macc->2mach, 1sam->1reg, 2sam->2reg,
1kgs->3reg, 2kgs->4reg, jdt->jdth, zeph->soph, obad->abd, jonah->jon,
hos->osee, mic->mich.

Re-verified through the actual getBiblePlanReadings call chain this
time (2026-06-04, post-pentecost-01/Thursday, "1sam 9-10" now
resolves a real responsory). npm test (2025 passed) and
tsc --noEmit pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 18:11:36 -04:00
will 3b46a18d63 Add 14 more books to Matins responsory pool, fix 2 dropped entries
Deploy / deploy (push) Successful in 1m52s
User caught jdt (Judith) listed as unseeded despite having real
responsory content directly visible in the reference source -- and
already flagged present in this same session's own citation dump,
never followed up on. Self-audit after that catch found job had also
been extracted earlier this session but never committed to the file.

Added jdt, esth, job, eccl, hab, joel, zeph, obad, jonah, mal, hos, mic,
nah, and lam (Maundy Thursday's own Tenebrae responsories). 33 of 72
bible-plan books now seeded, up from 19.

npm test (2025 passed) and tsc --noEmit pass; live-verified via
getResponsoryForBook resolving real text for every new key.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 17:43:43 -04:00
will 816dcadf0d Fix wrong book-count estimate in Matins responsory pool notes (66 -> 72)
Deploy / deploy (push) Successful in 1m52s
"~66" was an eyeballed guess from a sort|uniq -c list, never actually
counted -- and coincidentally echoes the 66-book Protestant canon count,
a bad number to leave uncorrected in a Catholic office project's own
docs. The real count (grep -c distinct `book:` values across
data/hours/bible-plan/*.yml) is 72.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 17:28:22 -04:00
will 7cee7a1e8a Fix Matins responsory extraction bug, unblock 8 more books
Deploy / deploy (push) Successful in 1m50s
The per-book pool extraction script only matched a bare [ResponsoryN]
tag, missing the reference engine's underscore-suffixed [ResponsoryN_]
template convention that holds the real text (a bare tag often just
@:-references it, alongside a Cistercian-rubric variant). This wrongly
made 1sam/2sam/1kgs/2kgs/2macc look untranslated in English.

Fixed the script and hand-followed @Tempora/X:ResponsoryN cross-file
references to add 1sam, 2sam, 1kgs, 2kgs, 2macc, zach, amos, and agg --
19 of ~66 bible-plan books now seeded (up from 11).

npm test (2025 passed) and tsc --noEmit pass; live-verified via
getResponsoryForBook resolving real text for every new key at every
pool index.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 17:13:13 -04:00
will 91dcc2c972 Seed 8 more books in the Matins per-book responsory pool
Deploy / deploy (push) Successful in 1m59s
Added exod, prov, sir, sap, tob, ezek, dan, 1macc (11 of ~66 total now
seeded), each sourced from a genuine, non-overridden reference-engine
Tempora file with real bilingual Responsory text. sir/sap and dan reuse
prov's/ezek's own entries respectively, confirmed by the reference
engine's own week files @-referencing that same pool -- real evidence
this app's loose book-level responsory match is historically authentic,
not invented.

Skipped 1sam (macro-only responsory text, not worth resolving for one
book) and 2sam/1kgs/2kgs/2macc/zach/amos/agg (real Latin but no
reference-engine English translation at all).

npm test (2025 passed) and tsc --noEmit pass; live-verified via
getResponsoryForBook resolving real text for every new key.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 16:59:50 -04:00
will a570266028 Add per-Common-category Matins responsory pool, expand per-book pool
Deploy / deploy (push) Successful in 1m52s
The per-book responsory pool (Nocturn 1 scripture readings) only had `isa`
seeded; the much bigger, previously untracked gap was that 272 of 355
nocturn-readings/*.yml files (Nocturn 2/3 patristic readings) had no
responsory at all. Added a sibling per-Common-category pool, sourced from
the reference engine's static Commune text files, covering all 19 of vu's
real `common:` category ids (several confirmed aliases of the same
underlying file). Wired into matins.ts's reading assembly as a fallback
when a reading has no proper responsory of its own. Also expanded the
per-book pool with `gen` and `jer`.

Live-verified end-to-end (St. Francis de Sales, Ss. Cornelius/Cyprian, Ss.
Euphemia/Lucy/Geminianus). npm test (2025 passed) and tsc --noEmit pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 16:47:17 -04:00
will 2a98386bdd Log the Suffragium Paschale mechanism fix in TODO.md
Deploy / deploy (push) Successful in 1m52s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 15:37:59 -04:00
will 58b201dc23 Build the Paschaltide "Suffragium Paschale" mechanism
Throughout Paschaltide (Eastertide/Ascensiontide/Pentecost), two
things happen wherever they'd otherwise show: the ordinary
5-suffrage set (Cross/BVM/Joseph/Apostles/Peace) collapses into one
alleluia-form "Suffragium Paschale" (Lauds and Vespers each have
their own distinct antiphon here, unlike the ordinary suffrages,
which share identical text at both hours), and the ferial-Preces
litany drops entirely rather than being replaced. Live-verified
against the reference source's own [Suffragium] macro directly,
byte-identical between Monastic Tridentinum 1617 and Tridentine 1906.

Exported isPaschaltide(season) from resolve-common.ts (wraps the
pre-existing PASCHALTIDE_SEASONS set). resolveSuffrages gained a
third hourId parameter to pick the right antiphon; the Preces
omission is a small new check gated on the same isFerialOrVigil
predicate that already selects the ferial litany form.

Authored lauds-suffrage-paschale.yml and vespers-suffrage-
paschale.yml -- English on the Vespers antiphon is this project's own
translation (the reference engine leaves it untranslated) and marked
draft; everything else verified.

Closes the last item in TODO.md's "Mechanism gaps" priority tier.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 15:37:53 -04:00
will b90789cab3 Log the Lent Vespers verification in TODO.md
Deploy / deploy (push) Successful in 1m50s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 15:12:11 -04:00
will 7551d205b7 Mark the rest of Lent's Vespers/Lauds hymn content verified
Same shape as the earlier Lauds capitulum/responsory/versicle fix
this session: real content, just never live-query-verified. Live-
verified Vespers' whole bundle (capitulum/responsory/hymn/versicle,
including the hymn's own claimed Monastic word-swap) against a plain
Lenten Wednesday (2026-03-04). Lauds' own hymn and both hours'
doxology-per-annum files were already seen live earlier the same
session (checking lauds-hymn-saturday.yml's real content against a
Lenten Ember Saturday) -- byte-identical, just needed the status
flipped. No more draft Lent content anywhere in this app.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 15:12:04 -04:00
will 25dc73da1f Add Lent's still-draft Vespers/hymn content to the Open Work list
Deploy / deploy (push) Successful in 1m54s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 15:00:53 -04:00
will 4864992cc0 Log Ember recombine/Lent verification/Trinity Sunday fixes in TODO.md
Deploy / deploy (push) Successful in 1m54s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 14:50:39 -04:00
will aba89dc250 Model Trinity Sunday as a real feast, not just a cosmetic label
Trinity Sunday wasn't modeled at all: getDayLabel had a purely
cosmetic "Trinity Sunday" label overlay for the Sunday after
Pentecost, but resolveTemporalId just fell back to post-pentecost-01
(ordinary-sunday category), so any Duplex+ saint assigned that date
(e.g. the Queenship of the BVM, fixed May 31) won outright, real
Trinity Sunday content never appearing anywhere.

Added a real temporal-feasts/trinity-sunday.yml record (Easter+56,
Duplex II. classis, live-verified). Per direct instruction,
unconditional: true (same shape as christ-the-king.yml) -- a
principal Sunday shouldn't lose a same-rank fixed-date collision, and
the one real collision case (Queenship of the BVM) isn't
independently verifiable against Monastic 1617 at all (that calendar
doesn't carry the Queenship feast -- May 31 there is St. Petronilla
instead).

Authored the full office and registered trinity-sunday in
ALWAYS_OVERRIDE_TEMPORAL_IDS -- the same generic mechanism already
built for the Pentecost-cluster fix, no new code needed: collect,
Benedictus/Magnificat antiphons, Matins hymn ("Summæ Deus cleméntiæ"),
Lauds/Vespers chapter (shared, Rom 11:33) and hymn (shared, "O lux
beáta Trínitas", already the pre-Urban VIII text), each hour's own
distinct responsory/versicle -- all live-verified 2026-05-31.

Fixed 3 pre-existing tests written around the old cosmetic-label
behavior, two of which had never-verified guesses baked in as if
correct: day-label.test.ts's "names the anchor day itself" test and
its "outranks an active octave" test (was asserting "Duplex I Class",
a guess -- the real live-verified rank is Duplex II);
transfer.test.ts's St. Felix I test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 14:50:31 -04:00
will 2a611cc17b Mark Lent's own Lauds capitulum/responsory/versicle verified
Not actually homeless, contrary to an earlier session's claim: these
3 files already had the real content (Isa 58:1 / "Ipse liberávit me"
/ "Ángelis suis"), just marked draft since never live-verified.
Live-verified against both a Lenten Ember Saturday (2026-02-28) and a
plain Lenten Wednesday (2026-03-04) -- identical, confirming this is
genuinely season-wide content, not weekday-varying.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 14:50:08 -04:00
will 3d230b34f2 Recombine the remaining 5 Ember-day nocturn-readings files
Same fix as ember-september-friday.yml (already done): each file's
Lectio1-3 turned out to be one continuous, unbroken homily (Bede,
Gregory, or Ambrose's Commentary on Luke for the two Advent cases,
cross-referenced from the Annunciation's own Common and confirmed
contiguous by reading the source file directly), split only to fill
the fixed 3-lesson slot count. Recombined ember-september-wednesday/
-saturday and ember-advent-wednesday/-friday/-saturday into single
readings, keeping each one's first lesson's own responsory.

All 6 Ember days now recombined -- no remaining split-for-no-reason
readings in this family. Updated ember-days.test.ts's lesson-count
table accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 14:49:59 -04:00
will cefff90a34 Log the Ascension collect/Corpus Christi/Sacred Heart fixes in TODO.md
Deploy / deploy (push) Successful in 1m49s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 12:55:00 -04:00
will 30b3971190 Give Corpus Christi and Sacred Heart their own hymnody, all octave
Same gap and same generic mechanism fix as Ascension/Pentecost --
just needed the ids added to ALWAYS_OVERRIDE_TEMPORAL_IDS plus real
content. Both feasts have 3 genuinely different hymns, one per hour:
Corpus Christi's own are widely known outside this context too
("Sacris solémniis" at Matins, "Verbum supérnum pródiens" at Lauds
whose last 2 stanzas are "O Salutaris Hostia", "Pange, lingua,
gloriósi Córporis mystérium" at Vespers whose last 2 stanzas are
"Tantum Ergo").

Sacred Heart needed one source-track correction: Monastic Tridentinum
1617 doesn't observe this feast on this date at all (shows St.
Barnabas instead) -- used Divino Afflatu 1954 instead, this project's
own stated rubric track. Also live-verified a genuine structural
difference: Sacred Heart's own Lauds/Vespers have no responsory at
all in the source (straight chapter to hymn) -- not an authoring gap.
Fixed lauds.ts's and vespers.ts's own resolveOffice to omit the
responsory part entirely when its status is missing in both
languages, instead of always rendering an empty section.

Authored matins-hymn-{corpus-christi,sacred-heart}.yml,
lauds-{capitulum,responsory,hymn,versicle}-corpus-christi.yml,
vespers-{responsory,hymn,versicle}-corpus-christi.yml,
lauds-{capitulum,hymn,versicle}-sacred-heart.yml, and
vespers-{hymn,versicle}-sacred-heart.yml -- all live-verified,
2026-06-04 and 2026-06-12.

Fixed 2 pre-existing tests whose own dates now land inside these
newly-modeled octaves and were asserting the old fall-through
behavior as if correct: lauds.test.ts's FERIAL_MONDAY (moved off
2026-06-08) and vespers.test.ts's St. Margaret of Scotland test
(2026-06-09) -- its own title already anticipated this fix, just had
the wrong expected values until now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 12:54:55 -04:00
will 98aa411a1b Author Ascension's own collect
A separate, pre-existing gap: getDayCollect already looked up
ascension-collect via resolveOfficeWinner (the same mechanism this
session's earlier fix already covers), but the content file itself
never existed. Live-verified against Divinum Officium (Monastic
Tridentinum 1617), 2026-05-14.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 12:54:35 -04:00
will 8dd04ab318 Log the Pentecost hymnody fix in TODO.md
Deploy / deploy (push) Successful in 1m50s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 12:07:54 -04:00
will 5d500c4e1f Give Pentecost its own hymnody at Matins/Lauds/Vespers, all octave
Same gap as Ascension (pentecost-sunday is a temporal-feast id, not a
saint record) -- the mechanism fix from that commit is generic, so
this only needed the id added to ALWAYS_OVERRIDE_TEMPORAL_IDS plus
real content.

Unlike Ascension, Pentecost has 3 genuinely different hymns, one per
hour: "Jam Christus astra ascénderat" (Matins), "Beáta nobis gáudia"
(Lauds), "Veni Creátor Spíritus" (Vespers), each with its own real,
distinct chapter/responsory/versicle at Lauds vs. Vespers.

Authored matins-hymn-pentecost-sunday.yml, lauds-{capitulum,
responsory,hymn,versicle}-pentecost-sunday.yml, and
vespers-{responsory,hymn,versicle}-pentecost-sunday.yml (Vespers
reuses Lauds' chapter via the existing cross-hour fallback) -- all
live-verified against Divinum Officium (Monastic Tridentinum 1617),
2026-05-24. Confirmed the whole octave end-to-end via resolveOrdo,
including a real winning saint within the window (St. Augustine of
Canterbury) keeping his own hymn at Lauds/Vespers unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 12:07:48 -04:00
will 374d27858a Log the Ascension hymnody fix in TODO.md
Deploy / deploy (push) Successful in 1m52s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 11:08:16 -04:00
will 44dd4bb47a Give Ascension its own hymnody at Matins/Lauds/Vespers, all octave
`ascension` is a temporal-feast id, not a saint record, so
resolveOfficeWinner's octave-synthesis (which already handles
saint-based octaves like Assumption's/St. Lawrence's) never
recognized it -- Ascension Day itself and its whole octave silently
rendered the generic Paschaltide hymn at every hour.

Fixed the mechanism: added `ascension` to ALWAYS_OVERRIDE_TEMPORAL_IDS
(covers the feast day itself), and taught resolveOfficeWinner to also
synthesize a {kind:'temporal'} winner from an active octave with no
saint record but a real temporal-feast record (covers ordinary-feria
octave days; a real winning saint within the octave still correctly
keeps priority).

A third gap surfaced testing this: octaveGoverningPrivilegedDay's
correct-by-design "a privileged Sunday never accepts an octave
override" rule is live-verified wrong specifically for "Dominica
infra Octavam Ascensionis" (keeps the octave's own hymn/responsory,
just with its own distinct proper chapter). Rather than loosen that
shared gate for every octave, gave resolveLaudsHymn/resolveVespersHymn
a narrow, hymn-only intercept guarded to saint-less octaves, checked
before the season/weekday default.

Authored matins-hymn-ascension.yml, lauds-{capitulum,responsory,hymn,
versicle}-ascension.yml, and vespers-{responsory,hymn,versicle}-
ascension.yml (Vespers reuses Lauds' chapter via the existing
cross-hour fallback) -- all live-verified against Divinum Officium
(Monastic Tridentinum 1617), 2026-05-14.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 11:08:11 -04:00
will ab8d1d452d Log the Sunday/seasonal Matins hymn work in TODO.md
Deploy / deploy (push) Successful in 1m54s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 10:38:10 -04:00
will e7e19c530a Author Sunday's own Matins hymn and every season's (Advent-Paschaltide)
The "genuinely more complex, not-yet-understood two-hymn mechanism"
flagged for Sunday turned out simple: Day0 Hymnus ("Nocte surgéntes")
is just Sunday's plain per-annum hymn, and Day0 Hymnus1 ("Primo
diérum ómnium") is Septuagesima-tide's own separate seasonal hymn --
the same shape Advent/Lent/Passiontide/Paschaltide each already have.
Live-verified these 4 don't vary by weekday within their season (a
Lenten Sunday and a Lenten feria render identically). None of the 6
were authored at all before this -- every Sunday and every season day
not otherwise overridden by a winning saint/octave silently fell
through to Monday's own ferial placeholder text.

Authored matins-hymn-{sunday,septuagesima,advent,lent,passiontide,
paschaltide}.yml, all live-verified, self-contained with their own
real closing doxology baked in. Septuagesima is checked directly by
day.season in resolveMatinsHymn itself, deliberately not folded into
the shared seasonalOfficeSuffix (Lauds/Vespers have no matching
content yet -- resolveOfficeBundle there is all-or-nothing per feast,
so adding it would silently blank their chapter/hymn instead of
falling through to their current weekday default).

Known, flagged simplification: the generic Paschaltide hymn wrongly
applies during the Ascension octave, whose own real hymn isn't
authored anywhere yet -- a same-shape gap Lauds already has, not a
new regression. Updated 2 FERIAL_DATE (a Tuesday in Advent) hymn
assertions that had been asserting Tuesday's own ferial text --
Advent's own seasonal hymn correctly outranks the weekday cycle now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 10:38:05 -04:00
will 8307526f69 Log today's Ember Friday/Matins-hymn/Marian Saturday work in TODO.md
Deploy / deploy (push) Successful in 1m45s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 09:34:26 -04:00
will 0b6a0ad1cc Complete Marian Saturday: proper Matins/Lauds hymns, ferial antiphons
Design (direct instruction): Marian Saturday is its own rank tier,
between simplex and vigil in vu's FeastClass scale -- beats a plain
Simplex saint (commemorated in return), loses cleanly with zero trace
to anything Vigil-or-higher. Deliberately not the full historical
Officium Sabbatorum B.M.V. ("no little office"): ferial psalmody is
kept everywhere, only the antiphons swap to Marian text; Matins and
Lauds each get a real proper Marian hymn; the Marian collect applies
whenever the day wins.

The calendar-side mechanism (calendar/index.ts's applyMarianSaturday)
and a collect/Benedictus antiphon already existed from an earlier
session -- authored what was still missing: matins-hymn-marian-
saturday.yml (Matins had nothing at all) and lauds-{capitulum,
responsory,hymn,versicle}-marian-saturday.yml.

Also found and fixed 3 real bugs from that earlier, unverified pass:
Lauds' own hymn ("O gloriósa Dómina") was sitting under the plain
ferial lauds-hymn-saturday id instead of its own marian-saturday id;
that id's capitulum/responsory/versicle siblings were live-verified
from a different, privileged Lenten Ember Saturday, not a genuine
per-annum ferial Saturday -- corrected to the real content ("Auróra
jam spargit polum" / Rom 13:12-13 / "Sana ánimam meam" / "Repléti
sumus mane misericórdia tua", live-verified against a clean September
Ember Saturday); and the Lauds psalmody override used the real
historical proper psalm set (92/99/62) instead of the ferial one,
rolled back per direct instruction to plain ferial psalms (50, 142,
the split Canticle of Moses) with only the antiphons swapped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 09:32:25 -04:00
will b8cb636446 Give the Matins ferial hymn its own text per weekday, not one for all
matins-hymn-ferial.yml turned out to already be Monday's own hymn
("Somno reféctis ártubus"), mislabeled as a single fixed year-round
text and used unconditionally for every weekday -- the source
(Psalterium/Special/Matutinum Special.txt) genuinely varies this hymn
Monday through Saturday, the classic "week of hymns". Caught first on
Friday ("Tu Trinitátis Únitas" expected, Monday's text shown), then
again on a real Ember Saturday ("Summæ Deus cleméntiæ" expected).

Authored matins-hymn-ferial-{tuesday,wednesday,thursday,friday,
saturday}.yml (Monastic 1617 text, live-verified against the CGI
instance directly, including Friday's own real M-track word-swaps) and
wired resolveMatinsHymn's final fallback tier to pick by day.weekday.
Sunday is left on the existing placeholder, documented as a real,
more complex not-yet-understood gap (a two-hymn source split).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 09:32:05 -04:00
will 4b6d55d56c Recombine Ember September Friday's split Matins homily into one reading
Lectio1-3 in ember-september-friday.yml were one continuous, unbroken
Gregory homily on Luke 7:36-50, split only to fill the fixed 3-lesson
Nocturn slot count -- the "source doesn't genuinely split" case this
store's own established default (post-pentecost-15.yml) already
recombines elsewhere. User caught this live on /2026-09-18/matins
showing 3 identically-labeled lessons in a row. Keeps Lectio1's own
responsory per the "first one, when several collapse" convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-04 09:31:51 -04:00
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
will bcbc93732f Log the Sunday Matins Gospel-pericope sweep in TODO.md
Deploy / deploy (push) Successful in 1m54s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-03 17:07:14 -04:00
will 750bea4cff Add full Gospel pericopes to post-Pentecost Sunday Matins readings
Closes the gap this whole sweep started from: Nocturn 3 on
post-pentecost-13/14 showed St. Augustine's homily with no Gospel
citation or text above it. Fixes all remaining post-pentecost-NN ids
(01-14, 16-24; -15 already had this from the original mechanism proof)
the same way as the rest of the sweep, plus Trinity Sunday's second
Nocturn 3 pair — the "Commemoration of the First Sunday after
Pentecost," whose Gospel (Luke 6:36-42) has no live date to source from
since Trinity always occupies that slot, so it's sourced from the
reference engine's preserved pre-displacement Mass propers file
instead. Also fixes a stray-space formatting glitch in
post-pentecost-19's existing homily text, found while sourcing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-03 17:07:10 -04:00
will 1dc8e3700a Add full Gospel pericopes to Eastertide Sunday Matins readings
Same gap and fix as the Advent/Christmas and post-Epiphany sweeps, for
easter-octave, easter-3 through -6, and sunday-after-ascension. Also
fixes stray leading "v." verse-marker artifacts left over in three of
the existing English homily texts (easter-4/5/6), found while sourcing
the paired Gospel text alongside them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-03 17:06:46 -04:00
will 01ffb25057 Add full Gospel pericopes to post-Epiphany through Palm Sunday Matins readings
Same gap as the Advent/Christmas sweep: Nocturn 3's patristic homily
appeared with no Gospel pericope before it. Authors the full text for
post-epiphany-1 through -6, septuagesima/sexagesima/quinquagesima,
lent-1 through -4, passion-sunday, and palm-sunday.

Palm Sunday needed a different source than the rest: its Missale
[Evangelium] tag is the Passion narrative (Matt 26-27) proclaimed at
Mass, not the entry-into-Jerusalem pericope (Matt 21:1-9) its own
Nocturn 3 homily is actually about — that shorter Gospel is instead
read at the Blessing of Palms, sourced from there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-03 17:06:41 -04:00
will b7b5a63756 Add full Gospel pericopes to Advent/Christmas Sunday Matins readings
Nocturn 3 previously dropped the bare "Et réliqua" Gospel incipit and
kept only the patristic homily, so the Gospel citation and text never
appeared. Authors the full pericope before the homily for advent-1
through advent-4 and christmas-octave-sunday, sourced from the
reference engine's Missale Gospel files. Authoring Advent 4's Gospel
surfaces a genuine second Matins Gospel on the rare Dec-24-as-Advent-4
collision (2023/2028), so the affected test now expects both readings
instead of one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-03 17:06:33 -04:00
will d8bb32bf45 Label unattributed hagiographic Matins readings by saint, not "Reading"
Deploy / deploy (push) Successful in 1m54s
A saint's vita reading (Nocturn 2, no patristic `source` attribution)
fell through to the UI's generic "Reading" heading. Add
fallbackHagiographicLabel, which looks up the contributing id's own
saint/temporal-feast record and produces "On St. Pius X, Pope and
Confessor" style labels instead — same plain-string convention as an
authored `source`.

Updates two matins.test.ts assertions that used label shape (string
vs object) as a proxy for "not a bible-plan reading" — no longer
reliable now that vita readings also carry a string label, so they
key off citation presence instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-03 16:16:46 -04:00
will 34c585726d Fix scripture-plan Gospel readings labeled generic "Gospel"
Deploy / deploy (push) Successful in 1m51s
Bible-plan Gospel entries in buildReadingPool never had an authored
label, so they fell back to the UI's generic "Gospel" heading. Reuse
getGospelIncipitFromCitation (already used for proper nocturn-reading
Gospels) to derive the real "A reading from the Holy Gospel
according to..." incipit from the citation when no label was
authored.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-03 16:07:26 -04:00
will 359c7bfc49 Log the seasonal hymn-doxology work in TODO.md
Deploy / deploy (push) Successful in 2m2s
Closes the "unexamined gap" open-work item and records both passes (the
source-marker audit, then the deliberate broadening) as dated build-log
entries, including the extraction-script bug caught and reverted before
anything was committed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-03 15:00:26 -04:00
will 46b34493e2 Wire seasonal hymn-doxology into every plain-default Lauds hymn
Lauds' Monastic source never carries the "*" substitution marker anywhere
(audited), so this is purely the same deliberate broadening as the Vespers
commit: every plain-default hymn (Sunday, Mon-Sat, Advent, Lent,
Passiontide, Paschaltide) now routes its closing doxology through
getHymnDoxologyId/appendDoxology instead of a fixed ending, split into
matching per-annum companion files that reproduce the original text
whenever no season/feast override applies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-03 15:00:17 -04:00
will 0736c36494 Wire seasonal hymn-doxology into every plain-default Vespers hymn
Sunday's hymn ("Lucis Creator") is the one case whose Monastic source text
carries a real "*" seasonal-substitution marker (live-checked); every other
key (Mon-Sat, Advent, Lent, Passiontide, Paschaltide) is wired the same way
anyway as a deliberate broadening beyond the source, at the user's request
("the doxology is a neat way to stay tuned in to the season... we're doing a
remix, not a copy"). Each hymn's own natural ending is split into a matching
per-annum companion file, so a season/feast with no override reproduces the
original text exactly; only Nat/Epi/Pasch/Asc/Pent/Corp/Heart and the two
feast/date overrides from the previous commit actually change anything.

Real visible effect: hymns shown during Christmastide/Epiphanytide/Corpus
Christi/Sacred Heart (none of which get a distinct seasonal hymn, so these
were always the plain weekday defaults) now pick up the right doxology
instead of a generic ending; Ascensiontide and the Pentecost octave now
diverge from the rest of Paschaltide's doxology mid-season.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-03 15:00:06 -04:00
will aeda043577 Wire seasonal hymn-doxology into the Matins ferial hymn
The plain ferial hymn's Monastic source (Day1-6 Hymnus,
Psalterium/Special/Matutinum Special.txt) carries a real "*" seasonal-
substitution marker, live-checked against the reference engine. Splits the
hymn's own natural doxology ending into a new per-annum companion file and
swaps it via getHymnDoxologyId/appendDoxology, same pattern already used by
Prime and Compline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-03 14:59:49 -04:00
will 44feb8c6b1 Widen seasonal hymn-doxology to cover Marian Saturday and the Assumption octave
getHymnDoxologyId now takes the whole LiturgicalDay (needed for a date-window
check) and checks a feast override for a temporal winner's id too, not just
sanctoral — matching Divinum Officium's own Common-of-the-BVM `Doxology=Nat`
rule, which is why Marian Saturdays get the Nativity doxology regardless of
season. Also ports the source's Aug 16-22 (Assumption octave) hardcoded
Nativity-doxology window, confirmed live not excluded for the Monastic/
Tridentine 1617 track.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-03 14:59:32 -04:00
will be4759d413 Split date picker/audio out of TODO.md's mechanism gaps
Deploy / deploy (push) Successful in 1m48s
They're new product surface, not something blocking a content pass —
give them their own "New features" tier instead of "Mechanism gaps".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-03 14:10:46 -04:00
will 998c549e51 Replace generic "Reading"/"Gospel" headings with real incipits
Deploy / deploy (push) Successful in 1m50s
Bible-plan readings now carry a generated bilingual incipit label
(bible-book-incipits.ts) instead of falling through to the UI's generic
"Reading" heading. Gospel pericopes (whether from the user's own plan
or nocturn-readings.ts's proper content) get the same treatment via a
new label field on the 'gospel' ResolvedPart, recovering the book from
the reading's own citation when no raw book code is available.

Updates 3 matins.test.ts assertions that used "no label" as a stand-in
for "this is a bible-plan reading" — no longer true now that these
readings carry a real label, so they now key off the label's shape
(string = patristic attribution, object = generated incipit) instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-03 14:02:58 -04:00
will 56321ab276 Add traditional book incipits for scripture reading labels
Maps every book abbreviation under src/data/scripture to its bilingual
liturgical incipit (e.g. "Léctio libri Sapiéntiæ" / "A reading from the
book of Wisdom"), sourced from the reference engine's own headers where
available. English uses "Saint Paul"/"Saint James"/etc. rather than
"blessed" per common modern usage; Latin keeps the traditional "beáti".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-03 14:02:49 -04:00
will 64a5634557 Collapse consecutive bible-plan chapters into a citation range
Deploy / deploy (push) Successful in 1m51s
Multi-chapter readings (e.g. Job 34-37, authored as one YAML entry per
chapter) were rendering as "Job 34; Job 35; Job 36; Job 37" instead of
the compact "Job 34-37". Verse-qualified and non-consecutive citations
are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
2026-09-03 11:46:34 -04:00
will d660f9e531 Give post-pentecost-15's homily reading its own Duo Séraphim responsory
Deploy / deploy (push) Successful in 1m46s
Every other post-Pentecost week's own file already attaches "Duo
Séraphim" (the closing responsory before Te Deum) to the hour's *last*
reading, overriding the usual "first one, when several collapse"
convention for exactly this responsory. post-pentecost-15 was the only
week not following that pattern -- its last (homily) reading kept
Lectio 10's own responsory instead. Swapped it in, matching the other
23 weeks; removes the now-resolved "needs an hour-level responsory
slot" note from TODO.md, which was chasing a problem that didn't
actually exist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Je2mXNHoXmi1DS3xEL67rH
2026-09-03 07:42:02 -04:00
will 10fd8515b9 Correct and relocate the Duo Seraphim and temporal-categories notes
Deploy / deploy (push) Successful in 1m51s
Duo Seraphim: the old note claimed it "isn't modeled" as a blanket
statement, but it's actually authored as a regular reading's own
responsory in 23 of 24 post-Pentecost weeks -- only post-pentecost-15
lacks it (its 3 readings don't include the one it historically closes).
Rewrote to state the real scope and moved to Mechanism gaps: the actual
gap is the schema having no hour-level closing-responsory slot, not a
missing content item.

temporal-categories.yml: the old note said the whole season->category
mapping was unverified, but the 2026-09-01 sweep (see that file's own
header) already fully live-verified `bySeason`. What's still open is
narrower -- a few privileged-period tier classifications (Ash Wednesday/
Holy Week's major tier on prior knowledge, Vigil of Christmas/Pentecost
Ember days defaulted as a guess, Pentecost Ember days' precedence
flagged in-file as a real unverified gap).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Je2mXNHoXmi1DS3xEL67rH
2026-09-03 07:38:13 -04:00
will 1725ce9186 Further trim TODO.md's Open Work per follow-up feedback
Deploy / deploy (push) Successful in 1m51s
- Removed the getDayCollects multi-collect-rendering note entirely.
- Moved the suffrage/ferial-Preces Paschaltide-substitution gap into
  Mechanism gaps, trimmed to just the actual gap (the alleluia-form
  "Suffragium Paschale" swap isn't modeled) rather than the prior
  verification-status framing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Je2mXNHoXmi1DS3xEL67rH
2026-09-03 07:34:50 -04:00
will 3794903c68 Alias common-of-a-confessor-pope to common-of-a-confessor-bishop
Deploy / deploy (push) Successful in 1m51s
While auditing the Lauds/Vespers Common-category psalmody note for
staleness, found 3 saints (St. Pius X, St. Leo II, St. Pius V) use
common: common-of-a-confessor-pope, which had no alias in either
category file -- unlike common-of-a-confessor-doctor, which already
does. Live-verified (votive=C4 vs C4b, both Vespers and Lauds,
Monastic Tridentinum 1617): psalms/antiphons are byte-identical to
Confessor-Bishop's, only the Magnificat antiphon and collect differ
(both already handled per-saint, not through this table).

Note: this doesn't change any currently-visible content -- all 3
saints are ranked duplex/semiduplex, below the duplex-majus+ threshold
this psalmody-override tier is gated at, so they correctly fall to the
plain ferial default regardless. It closes a real data-completeness
gap for whenever a duplex-majus+ Confessor Pope is added.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Je2mXNHoXmi1DS3xEL67rH
2026-09-03 07:26:19 -04:00
will c412086e0a Clean up TODO.md's Open Work section on request
- Removed the bogus St. Michael Sep 29 note from section 3 (user: "it's
  bogus").
- Removed the ferial-Preces note from section 3 -- it documents a design
  decision, not a simplification; moved to CLAUDE.md's "Not a
  reconstruction" list and a new memory entry instead.
- Moved the hymn-doxology note from section 3 to section 1 (mechanism
  gaps) and broadened its scope: checked which hours actually wire in
  the existing getHymnDoxologyId/appendDoxology mechanism (only Prime
  and Compline do), confirmed Terce/Sext/None are genuinely fine as
  documented, and found Lauds/Vespers/Matins are all equally
  unexamined -- not just the "5 of 30 Vespers hymns" the old note
  undersold this as.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Je2mXNHoXmi1DS3xEL67rH
2026-09-03 07:26:11 -04:00
will 2e39fd508a Remove completed items from TODO.md's Open Work section
Deploy / deploy (push) Successful in 1m56s
The dated log at the bottom already carries full detail for each; the
Open Work section should list only what's actually open, not linger on
struck-through DONE bullets (Easter's own octave, bilingual header
nameLa, Matins temporal-cycle readings, bulk scripture import).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Je2mXNHoXmi1DS3xEL67rH
2026-09-03 07:14:14 -04:00
will aa287ab806 Recover isolated single-verse scripture gaps that were transient, not real
Deploy / deploy (push) Successful in 1m57s
33 chapters from the first-pass "Unbound Bible" import each had one lone
missing English verse (matt-12:43's in-file "Transfer interrupted!"
download-corruption artifact among them). Rather than assume these were
genuine source gaps, checked every one against drbo.org's per-verse
text: 33 of 34 turned out to be real, present verses the old scrape had
simply dropped, now filled in.

The remaining 10 are confirmed genuine: drbo.org's own Douay-Rheims
numbering for these chapters stops one verse short of the Vulgate's own
count too (checked each chapter's actual max verse number there) -- a
real versification difference, not a scrape artifact, so these stay
honestly missing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Je2mXNHoXmi1DS3xEL67rH
2026-09-03 06:52:40 -04:00
will b29ce0551f Re-source the corrupted/missing scripture chapters from drbo.org
Deploy / deploy (push) Successful in 1m53s
The "Unbound Bible" source used for the previous pass had real defects
in 12 chapters (Douay-Rheims verses shifted/merged under the wrong
number, e.g. "Isaias 25:1" actually holding 25:11's text) and was
missing Daniel 4-14/Esther 11-16 outright.

Found a better source: drbo.org's combined "DR + LV" page per chapter
keeps each verse's Latin and English in the same HTML block, making
the earlier misalignment structurally impossible. Fetched and parsed
all 28 real chapters from it (bar-2; isa-5/25; joan-15; rom-9/11;
sir-10/19/23/41/51; dan-4..14; esth-11..16), spot-checked several
against known-correct text, and confirmed sir-52 isn't a real chapter
at all (Sirach only has 51 -- the old source's "52" was bogus).

Store now at 1185 chapter files with zero remaining gaps against every
book/chapter citation the app actually uses. Repoints
scripture.test.ts's "unauthored chapter" case from dan-14 (now
authored) to gen-51 (Genesis only has 50 chapters).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Je2mXNHoXmi1DS3xEL67rH
2026-09-03 06:41:17 -04:00
will 4bc5fdd77f Complete the bulk Vulgate/Douay-Rheims scripture import from the real source
Deploy / deploy (push) Successful in 1m49s
Found 137 chapters missing from src/data/scripture/ despite the recent
bulk import, scattered oddly across ~30 otherwise-covered books. Located
the original "Unbound Bible" source archives (douay_rheims.zip/latinv.zip)
the earlier import must have used but never preserved in-repo, and
re-parsed directly from them.

118 new chapters authored, plus the 3 hand-authored short books (Jude,
Obadiah, Philemon) replaced with source-derived text — small wording
differences from the hand-typed drafts confirm going back to source was
worth it. Store now at 1157 chapter files.

Deliberately left unauthored rather than risk bad content:
- Daniel 4-14 and Esther 11-16: this digitization's Latin side only has
  Daniel 1-3 and Esther 1-10 at all (English has the rest).
- 12 chapters where the Douay-Rheims side has a corrupted, misnumbered
  block (e.g. isa-25's tagged verse 1 actually carries verse 11's text)
  rather than simply missing content -- importing under the source's own
  wrong verse numbers would silently attach mismatched text to real
  citations, worse than leaving them missing.

Widened bulk-import.test.ts's invariant to tolerate up to 2 genuinely
isolated missing-English verses per chapter (extends the pre-existing
isa-33/sir-36 pointing-split-half-verse allowance to also cover true
source gaps, e.g. an in-file "Transfer interrupted!" download-corruption
artifact at matt-12:43), while still failing loudly on anything
resembling the corrupted-block pattern above.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Je2mXNHoXmi1DS3xEL67rH
2026-09-03 06:13:56 -04:00
will cd53805c83 Author Jude, Obadiah, and Philemon — books the bulk scripture import missed
Deploy / deploy (push) Successful in 1m45s
Three single-chapter books referenced by bible-plan.ts's readings had no
corresponding file in src/data/scripture/ at all, unlike everything else
covered by the Vulgate/Douay-Rheims bulk import — apparently dropped by
whatever produced the source. Authored by hand (Clementine Vulgate /
Douay-Rheims 1899 American Edition) and wired via new BOOK_ALIASES
entries (jude->jud, obad->abd, philem->phlm) so bible-plan's existing
citations keep their familiar English display form.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Je2mXNHoXmi1DS3xEL67rH
2026-09-03 05:51:20 -04:00
will 493c532469 Drop verse-number prefixes from scripture-sourced Matins readings
Deploy / deploy (push) Successful in 1m47s
resolvePassages() prepended each verse's number ("1 Et factum est...")
when joining scripture verses into continuous reading text — visible
today in Job's Matins reading. Not wanted; join the verse text alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Je2mXNHoXmi1DS3xEL67rH
2026-09-03 05:43:12 -04:00
will 63f48d26d2 Fix Matins bible-plan readings stuck on stale "translation pending"
resolveReading() in bible-plan.ts preferred each YAML row's hardcoded
status: { la: missing, en: missing } over computing status from the
actually-resolved text. That field was accurate before the Vulgate/
Douay-Rheims bulk import but was never cleared once real text landed,
so every Matins bible-plan reading kept showing as untranslated
regardless. Strip the stale field from all 368 files so status falls
through to the real compute-from-resolved-text path.

Also alias book abbreviations in scripture/index.ts's lookup: the
bulk-imported store uses Vulgate-numbering codes (sap, jac, joan,
1reg..4reg, 1par/2par, 1esdr/2esdr, ...) matching octave-readings.ts's
existing convention, while bible-plan.ts's TSV import used common
English abbreviations (kgs, sam, chr, john, jas, luke, mark, ...) for
the ~30 books where the two conventions diverge. Alias at the lookup
layer so citations keep their familiar English display form.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Je2mXNHoXmi1DS3xEL67rH
2026-09-03 05:43:07 -04:00
will 5ebb856de7 Bulk-import the Vulgate/Douay-Rheims scripture text
Deploy / deploy (push) Successful in 1m54s
Adds ~1017 chapter YAML files under src/data/scripture/ carrying verified
bilingual (Latin Vulgate / Douay-Rheims English) verse text, closing out
the bulk scripture import TODO.md has tracked as pending bulk-content
work. Daniel 14 (the Greek deuterocanonical addition) has no counterpart
in the Latin source and stays unauthored.

Raises the PWA precache size cap (8 -> 24 MiB) to fit the larger bundle,
repoints scripture.test.ts's "unauthored chapter" case from Genesis 1
(now authored) to Daniel 14, and adds bulk-import.test.ts asserting every
imported chapter has verified, gap-free, ordered verse text in both
languages -- tolerating the pre-existing isa-33/sir-36 pointing-split
half-verses, whose English intentionally isn't split to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Je2mXNHoXmi1DS3xEL67rH
2026-09-03 05:31:11 -04:00
will 90665429ae Author Easter Octave Wednesday-Saturday's shared Matins antiphons
Deploy / deploy (push) Successful in 1m28s
All 4 weekdays share the exact same 2 antiphons verbatim ("Jesum qui
crucifíxus est quǽritis, allelúja..." / "Veníte et vidéte locum ubi
pósitus erat Dóminus, allelúja, allelúja."), live-verified against
Pofficium.pl (Monastic Tridentinum 1617, prayMatutinum) across
2026-04-08/09/10/11 — despite each day drawing on a different Gospel
reading in the nocturn-readings pool, the antiphon layer doesn't vary
by day.

Layered onto vu's own already-fixed per-weekday ferial psalm numbers
rather than the reference engine's own differently-sized (Rule of
Benedict summer-contracted) psalm set for these days, consistent with
this project's deliberate rejection of that contraction.

This closes "Easter's own octave" fully: calendar precedence
(2026-09-01), Sunday/Monday/Tuesday's shared proper psalmody, and now
Wednesday-Saturday's antiphons (both 2026-09-02).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X26BkFT3ARbtUGXuBmzUmD
2026-09-02 07:51:34 -04:00
will f78222ce9e Let a temporal winner's ferial branch use its own named antiphons
Ferial-side counterpart to the Sunday-branch temporal-override
mechanism added earlier today: hours/matins.ts's ferialAntiphonedNocturn
now checks a new matins-ferial-named-antiphon-overrides.yml store first,
before falling back to the plain per-weekday default. An override's
antiphon list is distributed evenly across the weekday's own
already-fixed psalm-group count rather than tied to specific psalm
numbers, since one entry is meant to be reused across several
different weekdays (each with a different psalm set) instead of
authored per-day. Same optional weekdays field as the Sunday-side
override restricts which days of a multi-day temporalId span actually
share the content.

General-purpose, not scoped to any one feast — content added next.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X26BkFT3ARbtUGXuBmzUmD
2026-09-02 07:51:06 -04:00
will 57acbeefe9 Author Easter Sunday/Monday/Tuesday's shared proper Matins
Deploy / deploy (push) Successful in 1m29s
Easter Sunday, Monday, and Tuesday of the Octave share winner {kind:
temporal, id: easter-sunday}, and now share one live-verified proper
Matins psalmody: Nocturn 1 (Ps 1,2,8,15,23,27 under one antiphon),
Nocturn 2 (Ps 29,63,65,75,87,107 under one antiphon), Nocturn 3 (the
same 3 canticles already authored for the ordinary-Paschaltide
seasonal override, under its own antiphon) — sourced from
Pofficium.pl (Monastic Tridentinum 1617, prayMatutinum) against
2026-04-05/06/07. All 3 nocturn-closing versicles are byte-identical
to the ones already authored for the Paschaltide override, confirming
those are shared season-wide rather than coincidentally reused. The
12 patristic lessons were already authored in an earlier sweep
(nocturn-readings/easter-sunday.yml) and needed no new work.

Restricted to just these 3 days via the new weekdays field: the
Octave's other 4 weekdays (Wed-Sat) share the same winner id but have
their own different, not-yet-authored antiphons, tracked as a
separate open item in TODO.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X26BkFT3ARbtUGXuBmzUmD
2026-09-02 07:42:39 -04:00
will c2b24ae77d Let a temporal winner reach Matins' 3-nocturn proper psalmody
hours/matins.ts's threeNocturns gate and Sunday-branch dispatch only
ever fired for a literal Sunday or a sanctoral winner — a temporal
winner (e.g. Easter Monday/Tuesday, sharing Easter Sunday's own
winner id) had no path into 3-nocturn proper content at all, even
with a matching named override authored for it.

Generalizes hours/matins-sunday-named-nocturn-overrides.yml's lookup
so a temporal winner with an authored entry is eligible too, reusing
the same sundayPsalmNocturn/sundayCanticleNocturn render functions
the Sunday and saint-proper branches already share. A new optional
weekdays field lets one entry restrict itself to specific days of a
temporalId that spans more calendar days than actually share its
content (needed by the Easter content this unblocks, added next).

Built as a general-purpose feature, not scoped to Easter specifically
— any temporal feast that wants its own proper psalms/lessons can now
reach this path by adding a named-override entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X26BkFT3ARbtUGXuBmzUmD
2026-09-02 07:42:06 -04:00
will f4d4eb3738 Lower Matins 3-nocturn threshold from Duplex to Semiduplex
Deploy / deploy (push) Successful in 1m31s
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) — the previous Duplex floor meant a Semiduplex winner like
St. Stephen of Hungary wrongly fell all the way through to the
1-nocturn ferial branch.

New threshold is a separate check from isDoubleOrHigher, which drives
the unrelated antiphon-doubling rule elsewhere and stays floored at
Duplex — the two thresholds only coincided by accident before this
change. No new content authoring was needed: ferialPsalmodyThreeNocturns
already chunks the ferial 9-psalm pool into 3/3/3 as the fallback for
a plain-Duplex winner with no override authored, and Semiduplex
winners now route into that same already-working branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X26BkFT3ARbtUGXuBmzUmD
2026-09-02 07:07:12 -04:00
will 9da5a74301 Remove spurious mid-nocturn versicle from ferial Matins
The 1-nocturn ferial branch was rendering a V./R. versicle stuck
mid-list, as if marking a nocturn boundary that doesn't exist in a
single continuous nocturn. Live-verified against the reference engine
(Pofficium.pl, Monastic Tridentinum 1617) for Wednesday and Thursday:
the real office runs straight from the last psalm to the Capitulum,
with no versicle anywhere in the psalmody. The embedded mid-list V./R.
line in the source Psalmi matutinum.txt is inert there and was never
actually emitted by the engine — matins-ferial-antiphons.yml's
versicle field was a documented-but-unverified assumption, not real
content.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X26BkFT3ARbtUGXuBmzUmD
2026-09-02 07:06:21 -04:00
will 9d39d162ec Strip stray Vulgate verse-number markers from psalm text
Deploy / deploy (push) Successful in 1m28s
Several psalm files carried leftover inline markers like (6a), (17a),
(32) from the source text (e.g. Ps 67:2-19). vu doesn't number verses
inline anywhere else, so these were artifacts, not content.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yHHNsbzD8jLkkmVuJpAcC
2026-09-02 06:31:08 -04:00
will b54fcb30e6 Reclassify Passion week ferias from ordinary-feria to privileged-feria
Deploy / deploy (push) Successful in 1m35s
Passion week's ferias (Mon-Sat before Palm Sunday) fell through to
passiontide's bare ordinary-feria default, letting any real feast win
outright regardless of rank. Live-verified against the reference engine:
swept every date that can land in this window across a century of Easter
dates and found zero sub-Duplex winners -- every real winner is Duplex+.
This was letting vu's own low-ranked calendar saints (e.g. Ss. Tiburtius/
Valerian/Maximus, St. Anicetus) trivially win there and wrongly restore
the responsory's closing Gloria on what should stay a penitential day.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yHHNsbzD8jLkkmVuJpAcC
2026-09-02 06:21:02 -04:00
will 3f9b748e4a Fix Matins test part-index offsets for new opening versicle
Deploy / deploy (push) Successful in 1m28s
Inserting the new Domine-labia versicle shifted every downstream
ordo.parts index/destructure by one. Pure index-shift fixups, no
behavioral assertions changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yHHNsbzD8jLkkmVuJpAcC
2026-09-02 05:54:15 -04:00
will db23523a5d Add Domine labia mea aperies (x3) to Matins opening
The Monastic-rite versicle "V. Dómine, lábia mea apéries. R. Et os
meum annuntiábit laudem tuam" was entirely absent from Matins' opening
block — only the shared "Deus in adjutórium" versicle was modeled.
Authored per the reference engine's "iij" rubric (recited three times,
after Deus in adjutórium, before Psalm 3), with the English rendered
as imperative ("O Lord, open Thou my lips") rather than future
indicative.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yHHNsbzD8jLkkmVuJpAcC
2026-09-02 05:54:10 -04:00
will 5b3dc2e9d5 Redesign the Easter-octave backlog: floor-gated, one per day
Deploy / deploy (push) Successful in 1m28s
Replaced the multi-day cascading queue with the actual intended design:
only candidates at or above duplex-majus are worth carrying out of the
Palm-Sunday-through-Low-Sunday span at all - below that, a candidate
simply lapses for the year. Of those that clear the floor, the Nth day
after Low Sunday celebrates the Nth one (native-date order), winning
outright with no rank fight, demoting whatever's natively there to a
commemoration - one per day, positional, not a free-day search.

Caught a real bug while re-verifying: the ordinary single-hop transfer
check was independently leaking Low Sunday's own signal into the next
day, letting a below-floor candidate bypass the new gate. Fixed by
excluding the whole span from that path.

Inventoried the only duplex-majus+ candidates that can ever fall in the
reachable window (St. Benedict, St. Gabriel the Archangel, the
Annunciation, St. Mark, Ss. Philip & James), then bumped St. Patrick's
own rank to duplex-majus by direct instruction, adding him as the 6th.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
2026-09-01 15:06:55 -04:00
will fc4ac33903 Fix privileged-feria-major: nothing is ever commemorated, any rank
Deploy / deploy (push) Successful in 1m32s
The Easter octave cascade built last commit reused the existing
"duplex-majus+ still gets a bare commemoration" branch unchanged,
trusting its own citation (St. Mark within the Easter Octave). That
citation was a misreading. Live-verified: the Annunciation (Duplex I.
classis) transfers with zero commemoration on Holy Thursday itself, and
Chair of St. Peter at Antioch does the same on Ash Wednesday - the
exception was wrong everywhere this category is used, not just Holy
Week. privileged-feria-major now matches privileged-sunday exactly:
unconditional transfer, no exceptions, any rank.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
2026-09-01 14:08:15 -04:00
will d6c8e8b7a8 Build the Easter octave transfer cascade
Deploy / deploy (push) Successful in 1m32s
Closes the tabled "Easter's own octave" mechanism gap. resolveDay only
ever checked yesterday/tomorrow for an inbound transfer - a single hop -
so anything impeded in Holy Week just vanished rather than reappearing
after the Octave. Live-verified the real behavior first (2033 proof
year): a candidate skips the whole privileged span and keeps walking
forward, FIFO by native date, until it lands; a transferred-in candidate
never gets the "still gets a nod" commemoration a native occurrence of
the same rank would along the way (confirmed with the Annunciation, zero
commemoration anywhere in the span). Also fixed Low Sunday itself, which
needed the same privileged-sunday treatment as Easter Day.

Built resolveForwardTransferLanding: a bounded queue simulation that
reuses applyIncomingTransfer unchanged per hop - its existing
privileged-feria-major no-op was already the right single-hop refusal,
it just needed a real cascade around it instead of a dead end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
2026-09-01 12:25:21 -04:00
will 4adbb71ddf Full precedence sweep of temporal-categories.yml, two more real bugs
Deploy / deploy (push) Successful in 1m27s
Following the Ascensiontide fix, swept every remaining unverified Sunday/
feria precedence tier against the live reference engine:

- commemorations.ts's privileged-sunday rule was itself wrong: a
  duplex-majus+ candidate was given a bare commemoration instead of
  transferring, never actually verified. Live-checked the Immaculate
  Conception (transfers off Advent II) and the Annunciation (transfers
  off Palm/Easter Sunday) - confirmed correct by direct instruction.
  privileged-sunday now transfers every rank unconditionally.

- eastertide's own Sunday tier was backwards (privileged-sunday instead
  of ordinary-sunday) for the ordinary Sundays after Easter - live-
  verified twice (Finding of the Holy Cross, St. Mark both win outright).
  Easter Day itself still needs the stronger tier, now special-cased
  directly since Sundays never consult the offset table.

Six existing tests updated to reflect real, live-verified behavior - each
had the old wrong "commemorated in place" assumption baked into its
expected value. Remaining unverified entries (Vigil of Pentecost/
Christmas, Corpus Christi/Sacred Heart) documented as such rather than
silently assumed correct.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
2026-09-01 11:45:04 -04:00
will 6b66999908 Fix Ascensiontide Sunday precedence and give it its own Matins proper
Deploy / deploy (push) Successful in 1m27s
The Sunday within the Octave of Ascension was letting a Duplex saint win
outright (ascensiontide's Sunday was miscategorized ordinary-sunday
instead of privileged-sunday, the only "great octave" season not already
so classed). Live-verified the reference engine keeps the Sunday primary
and transfers the saint instead. Fixed the category, then authored the
Sunday's own wholly proper Matins (a different psalm scheme entirely, not
a seasonal overlay) via a new per-temporalId override layer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
2026-09-01 09:58:40 -04:00
will 05f82a3556 Give ordinary Paschaltide Sundays their own Matins Nocturn I-III content
Deploy / deploy (push) Successful in 1m26s
Same seasonal-overlay shape as Advent, live-verified against the reference
engine across 5 Paschaltide Sundays. Generalized sundayNocturnFor to a
season-keyed lookup covering both Advent and Paschaltide.

Confirmed two related non-gaps while investigating: Christmastide/post-
Epiphany Sundays have no seasonal block in the source, and a Duplex+ saint
winning outright on a Sunday deliberately still uses the fixed Sunday
psalmody per this app's own documented design (readings still surface the
saint via the separate pool) — neither needs a fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
2026-09-01 09:31:22 -04:00
will c3e540f6c2 Give Advent Sundays their own Matins Nocturn I-III content
Deploy / deploy (push) Successful in 1m27s
Nocturns I-II had no seasonal override mechanism at all, and Nocturn III's
own Septuagesima-Palm override didn't cover Advent — live-verified against
the reference engine that Advent has its own seasonal antiphons/versicles
across all three nocturns, identical on all 4 Advent Sundays.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
2026-09-01 09:07:46 -04:00
will 20b1401919 Log Sunday Nocturn 3 seasonal fix, close the flagged gap
Deploy / deploy (push) Successful in 1m27s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
2026-09-01 08:17:18 -04:00
will 3d221c2a45 Give Sunday Matins Nocturn III its own antiphon Septuagesima-Palm Sunday
It was hardcoded to "Allelúja, allelúja, allelúja" year-round. Each of
the 9 Sundays from Septuagesima through Palm Sunday has its own
Gospel-tied antiphon instead, live-verified against Monastic Tridentinum
1617; Lent 1-4 also swap in their own canticles, and Lent 1-4 plus
Passion/Palm Sunday swap in their own versicle. New
matins-sunday-nocturn3-overrides.yml, keyed by temporalId and merged
over the base nocturn3 group by hours/matins.ts's new
sundayNocturn3For — only the real-Sunday branch uses it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
2026-09-01 08:17:14 -04:00
will 36e2cc60cd Add Jeremiah 14, Lamentations 5, and Ezekiel 36 scripture chapters
Lent 1-4's Sunday Matins Nocturn III canticles, transcribed from a live
Divinum Officium query (Monastic Tridentinum 1617). First content under
the `lam` book code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
2026-09-01 08:17:04 -04:00
will 97b5a5d7f4 Log ferial antiphon fix and flag Sunday Nocturn 3 Alleluia bug
Deploy / deploy (push) Successful in 1m28s
Records the sourcing detail for the one-antiphon-per-psalm change in
the dated build log, and adds a mechanism-gap entry for Sunday
Matins' Nocturn 3 antiphon: hardcoded to "Allelúja" year-round but
confirmed live against Monastic Tridentinum 1617 to need its own
proper antiphon on each of the 9 Sundays from Septuagesima through
Palm Sunday — collected but not yet authored.
2026-09-01 07:30:44 -04:00
will afff77f75a Split ferial Matins nocturn antiphons to one per psalm
Every cross-psalm pair previously shared one antiphon under just the
first psalm, which read inconsistently once psalter-distribution.yml's
reslice left some pairs intact and split others. Each pair is now two
single-psalm groups: the psalm whose own words the shared antiphon
actually quoted keeps it, and its former partner gets a real antiphon
borrowed from elsewhere in the reference corpus where one exists, else
one composed fresh from its own opening verse.
2026-09-01 07:30:35 -04:00
will 8919497d24 Update tests for ferial Matins Sunday-leak fix
Deploy / deploy (push) Successful in 1m28s
The rest-of-year temporal nocturn-readings sweep was testing the Monday
after each named Sunday, relying on the now-removed leak to prove content
reachability for dates with no separately-authored ferial content -
repointed to the actual Sunday date. Updated the 2026-08-24 suppression
test's ferial case to assert the leak no longer happens, and added a
regression test pinning the original 2026-09-01 bug report.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
2026-09-01 07:06:20 -04:00
will 0f84c9e55b Stop ferial Matins from reusing the governing Sunday's own Nocturn 3 content
A plain feria within an ordinary week was pooling that week's Sunday's own
patristic homily and responsory verbatim into its single nocturn, because
day.winner.id (added unconditionally) equals the Sunday's own temporalId on
an ordinary day, bypassing the existing suppression gate. Both the
day.winner.id/commemoration shortcut and the plain temporalId/month-week
pool are now gated on threeNocturns, so a ferial day only draws on its own
day's content.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
2026-09-01 07:06:14 -04:00
will 54d1d6190e Update tests for T/S/N Preces ferial split
Deploy / deploy (push) Successful in 1m27s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
2026-09-01 06:39:14 -04:00
will b050e523ec Split Terce/Sext/None Preces into always-shown Kyrie/Pater + ferial-only versicles
Kyrie eleison and Pater Noster now render every day regardless of rank
(minor-hour-kyrie-pater); the further Preces V/R versicle pairs are
ferial-only, dropped on Sunday and any sanctoral winner.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
2026-09-01 06:39:10 -04:00
will 45c1f4e958 Update tests for Sept 1 saints removal
Deploy / deploy (push) Successful in 1m28s
september-sanctoral.test.ts: Sept 1 now resolves to a plain temporal
day with no commemorations; the Raymond Nonnatus transfer-collision
case now wins uncontested since the native Sept 1 saints are gone.

vespers.test.ts: the semiduplex-keeps-its-own-Vespers-against-a-
lower-ranked-tomorrow regression test used Raymond Nonnatus vs. St.
Giles as its example pair; re-pointed to St. Agatha (semiduplex) vs.
St. Dorothea (simplex), a different adjacent pair that exercises the
same concurrentia rule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
2026-09-01 06:24:20 -04:00
will 64271befee Drop St. Giles and Ss. Twelve Brothers from Sept 1
User decision to carry no saints on Sept 1. Data files for both
saints (and their propers/commemoration bundles) are left in place,
restorable by re-adding the sanctoral-calendar.yml list entry, same
pattern used when St. Hermes was dropped from 8/28.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XMSokiTD2Qc5vPP2YQu3Q
2026-09-01 06:24:14 -04:00
will e7c5fc0c24 Give a Vigil's own First Vespers Magnificat its real weekday fallback
Deploy / deploy (push) Successful in 1m31s
A Vigil never has a fixed proper Magnificat antiphon in the real rubric
-- its Rule block ("ex C1v; ... Laudes 2") always borrows whatever the
current ferial weekday's own antiphon is, live-verified across 4
different years (2025-2028) at the Vigil of St. Andrew's own First
Vespers. getMagnificatAntiphon now special-cases saint.rank === 'vigil'
to fall through to the same weekday-default table the plain temporal
branch already used, rather than trying (and missing) a fixed Common
lookup.

Closes the 11th and last Common-category Magnificat antiphon gap; see
TODO.md for the full writeup, including how the first pass wrongly
closed this one as "not a gap" before being asked to double check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QEe9oTbn1UjQVAYcCoW1a
2026-08-31 22:27:27 -04:00
will ccc406003c Author 10 sanctoral Common-category Magnificat antiphons
getMagnificatAntiphon's Common-category fallback
(magnificat-antiphon-${benedictusCommon}) had never been authored for
any of the 11 real Common categories saints fall back to -- only the
temporal-cycle side of this gap had been closed previously. Sourced
each from its Commune file's own [Ant 3] (Second Vespers), live-verified
against Ss. Placid and Companions, St. Blaise, and St. John Gualbert's
real evenings.

common-of-a-vigil (the 11th category) is handled separately, in code --
see the next commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QEe9oTbn1UjQVAYcCoW1a
2026-08-31 22:27:17 -04:00
will 8702955a5f Fix wrong Ant-tag sourced for two Common-category Benedictus antiphons
benedictus-antiphon-common-of-a-confessor.yml and -the-bvm.yml were each
reading their Commune source file's [Ant 1] (the Common's First Vespers
antiphon), not [Ant 2] (the real Lauds antiphon) -- discovered while
sourcing the sibling Magnificat antiphon set. Live-verified wrong via St.
Alexius's real Lauds query (renders "Euge, serve bone", not "Similabo
eum").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QEe9oTbn1UjQVAYcCoW1a
2026-08-31 22:27:08 -04:00
will e0156c1512 Fix Vespers concurrentia to compare today's rank against tomorrow's directly
Deploy / deploy (push) Successful in 1m27s
keepsOwnSecondVespers used a fixed duplex-2-classis floor on today's rank
alone, so a lower-ranked but still-real sanctoral winner (e.g. Simplex)
tomorrow could still steal First Vespers from a higher-ranked-but-below-the-
floor winner today (semiduplex St. Raymond Nonnatus losing to simplex St.
Giles on 2026-08-31). The real "De Concurrentia Officii" rubric compares the
two days' ranks directly, with ties going to tomorrow — replace the floor
with that comparison when both sides have a real sanctoral winner, keeping
the floor only as a fallback against a plain Sunday.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QEe9oTbn1UjQVAYcCoW1a
2026-08-31 22:01:33 -04:00
will 06fc8ef0ec Add missing commemoration bundle for St. Rose of Lima
Deploy / deploy (push) Successful in 1m29s
Her Aug 31 commemoration (e.g. under St. Raymond Nonnatus) was falling
back to a bare collect with no Ant./V./R. since she was authored after
the earlier commemoration-bundle sweep and never got her own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QC6jHsj75xcsTkapqJR6fD
2026-08-31 09:31:58 -04:00
will 1923433a29 Normalize versicle notation to plain V./R. across propers
Deploy / deploy (push) Successful in 1m31s
Unicode ℣/℟ (U+2123/U+211F) were mixed unpredictably with plain V./R.
across ~550 proper YAML files, sometimes within the same file. Screen
readers can't announce the unicode glyphs. Replace all instances with
plain text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QC6jHsj75xcsTkapqJR6fD
2026-08-31 09:23:30 -04:00
will a339068f81 Show a transferred saint's status in the day label
Deploy / deploy (push) Successful in 1m32s
getDayLabel now surfaces the transfer signal added in the previous
commit: a transferred-in winner gets "(Rank, transferred)" instead of
just "(Rank)"; a transferred-in commemoration gets "(transferred)"
appended to its bare name; and the date a saint transferred away from
gets a trailing "St. X (transferred away)" note so a reader isn't left
wondering why an expected saint is missing that day. Deliberately worded
differently on the two sides (plain "transferred" reads as "arrived via
transfer" next to a rank parenthetical; "transferred away" spells out the
direction on the origin date, where there's no rank alongside it to
anchor that reading) and deliberately omits a landing date on the away
note, per direct instruction, since a real landing can chain further than
the single adjacent day resolveDay actually checks (an unmodeled case,
e.g. a transfer running into Holy Week).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013g7AsSvMD9oazR17f3BxLz
2026-08-31 08:53:12 -04:00
will db9c844caf Track a transferred saint's origin date on DayWinner and Commemoration
Adds transferredFrom to both DayWinner's sanctoral variant and
Commemoration's sanctoral variant, set in calendar/index.ts's
applyIncomingTransfer whenever a transferred-in candidate actually lands
(wins outright) or ends up merely commemorated (lost a collision, or fell
below the landing day's own threshold) — previously this signal was
discarded after resolveDay used it once to decide the winner, so nothing
downstream could tell a transferred feast apart from a native one.

Also adds transferredAway to LiturgicalDay, set from a date's own native
occurrence when its candidate couldn't be kept there at all, so a UI can
note "this office moved elsewhere" without asserting a specific landing
date (which a later, unmodeled multi-hop chain — e.g. into Holy Week —
could get wrong).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013g7AsSvMD9oazR17f3BxLz
2026-08-31 08:53:02 -04:00
will cb60039ab2 Fix Sept sanctoral test for Raymond's new semiduplex rank
Deploy / deploy (push) Successful in 1m28s
Raymond's rank deflation (a1ed3d2) changes cascading behavior in years
where Aug 31 is a Sunday: he now loses to the Sunday outright (needs
duplex+ to win, per Divino Afflatu's ordinary-sunday threshold) and
transfers forward onto Sept 1, winning the collision there over St.
Giles. Move the original clean-year Giles test to 2026 and add a new
test documenting the 2025 transfer/collision case explicitly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013g7AsSvMD9oazR17f3BxLz
2026-08-31 08:23:08 -04:00
will a1ed3d2f9c Deflate St. Raymond Nonnatus's rank to semiduplex
Canonized 1657, well past the ~1600 late-canonization threshold; per
project policy his source Duplex rank gets deflated one tier, same
treatment already applied to St. Rose of Lima.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013g7AsSvMD9oazR17f3BxLz
2026-08-31 08:23:02 -04:00
will 514300821a Bulk-author nameLa for all 299 remaining saints, close bilingual header backlog
Deploy / deploy (push) Successful in 1m31s
Authors a real Latin nameLa for every saint in src/data/calendar/saints/
except the deliberate example-confessor placeholder (standard
ecclesiastical Latin forms, a lighter sourcing bar than this app's usual
reference-engine verification -- flagged for future spot-check). Fixes a
real mechanism bug found in the process: calendar/index.ts's sanctoral
collision-resolution branch was dropping nameLa for any saint that won
after beating another same-day candidate. Updates ~180 pre-existing test
fixtures across the calendar suite for the new field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwsZQMjALCvy7u9tFDWmuQ
2026-08-31 06:44:10 -04:00
will a6261f044a Add nameLa mechanism for bilingual header, proof set of 11 feasts + St. Lawrence
Deploy / deploy (push) Successful in 1m32s
Threads an optional nameLa field through SanctoralIdentity/SaintRecord/
TemporalFeastRecord/ActiveOctave so day-label.ts can render real Latin
proper names instead of always falling back to English. Authors the
proof set: all 11 named temporal feasts plus St. Lawrence (chosen to
exercise the octave-headline and octave-commemoration code paths too).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwsZQMjALCvy7u9tFDWmuQ
2026-08-31 06:30:48 -04:00
will 76cc363a75 Fix stale St. Damasus header comment on octave precedence
Deploy / deploy (push) Successful in 1m41s
Claimed Semiduplex wins outright over the Advent feria with the feria
commemorated in return -- true when written, but no longer true after
the later octave-wins-class rework. Dec 11 falls inside the Immaculate
Conception octave, whose elevated wins:duplex threshold now outranks a
plain Semiduplex, so Damasus is actually demoted to a commemoration
and the Advent feria keeps the day. No behavior change, found and
confirmed live while authoring St. Juan Diego's neighboring entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwsZQMjALCvy7u9tFDWmuQ
2026-08-31 06:00:42 -04:00
will 3ceb1e9df0 Add Our Lady of Guadalupe (Dec 12)
Sourced from the reference engine's national-calendar file. Own proper
collect, Ant 1, and Matins Nocturn 2 narrative reading (Juan Diego's
apparition account), same scope as the Our Lady of Lourdes feast --
Common of the BVM (C11) carries the rest. No canonization-deflation
question here (an apparition title, cultus confirmed 1754/1910). At
Duplex she ties the Immaculate Conception octave's threshold on a
non-closing day, which favors the occurring saint, so she wins Dec 12
outright.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwsZQMjALCvy7u9tFDWmuQ
2026-08-31 06:00:36 -04:00
will e1595e05ec Add St. Juan Diego (Dec 9)
Sourced from the reference engine's national-calendar file (canonized
2002, outside both Tridentine 1906 and Divino Afflatu 1954). Source
rank "Duplex optional" has no FeastClass slot; mapped to Duplex then
deflated one tier to Semiduplex per the late-canonization rule. Live-
verified he's actually a commemoration, not the day's winner: Dec 9
falls inside the Immaculate Conception octave, whose elevated
wins:duplex threshold outranks a plain Semiduplex.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwsZQMjALCvy7u9tFDWmuQ
2026-08-31 06:00:22 -04:00
will 2df1fd0d3b Add St. Rose of Lima (Aug 30), rank deflated for late canonization
Deploy / deploy (push) Successful in 1m37s
She was previously excluded on a mistaken "post-1617, out of scope"
reading that conflated vu's hour-structure track (Monastic 1617) with
its calendar/saints track (Tridentine 1906 + Divino Afflatu 1954,
which has included her since her 1671 canonization). Her source rank
of Duplex is deliberately deflated to semiduplex, correcting for the
rank creep that made "new canonization" and "Duplex" a near-automatic
pairing from the Baroque period on. She wins Aug 30 outright over Ss.
Felix and Adauctus, who keep their existing commemoration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwsZQMjALCvy7u9tFDWmuQ
2026-08-31 05:41:16 -04:00
will efb1717e27 Add test coverage and TODO.md log for the full temporal-cycle sweep
Deploy / deploy (push) Successful in 1m31s
Extends tests/hours/matins.test.ts's proof-date table to cover all 28
newly-authored temporal ids end-to-end, including Easter and Pentecost
Sunday. Documents the full sweep in TODO.md, including the
easter-sunday/pentecost-sunday misdiagnosis and correction so the
"1 nocturn in the source" vs. "no content" distinction doesn't get
re-litigated later.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 23:21:03 -04:00
will 73db52bd96 Author Easter and Pentecost Sunday's own Matins nocturn content
Both feasts' source keeps the ancient single-nocturn, 3-lesson Matins
form (unlike every other Sunday in the surrounding sweep), which was
initially mistaken for "no content exists" and left unauthored. Their
Lectio1-3 actually carry a real Gospel incipit plus a full Pope St.
Gregory the Great homily each (Homily 21 on Mark 16:1-7 for Easter,
Homily 30 on John 14:23-31 for Pentecost) — a nocturn-count difference,
not a content gap. Authored as nocturn:1 content, same as this app's
existing Ember-day single-nocturn files; this app's own threeNocturns
rule still renders 3 nocturns for these Sundays regardless.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 23:20:57 -04:00
will 1cb6ca16da Author Matins temporal-cycle nocturn readings for Advent through Ascensiontide
Extends the post-Pentecost nocturn-readings sweep to the other 26 temporal
ids spanning the rest of the liturgical year — Advent, Christmastide,
post-Epiphany, Septuagesima through Lent, Passiontide/Holy Week, and
Eastertide — transcribed directly from the reference engine's own
per-season Tempora source files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 23:20:47 -04:00
will 7f0b0f05df Move today's completed work from Open Work into the dated build log
Deploy / deploy (push) Successful in 1m30s
Was inline-editing "DONE" notes into the top-of-file priority list
instead of following TODO.md's own convention: completed work moves out
of "Open work" entirely and into a dated ### entry at the bottom. Fixes
that for everything done today that never got a proper log entry --
the Vigil of Christmas content, the Advent IV/Dec 24 Sunday collision
resolution, the full sanctoral commemoration bundle sweep (218/218),
and closing out the last two Vespers Magnificat "gaps" as not gaps.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 18:40:13 -04:00
will f1f46e2573 Close the two remaining Vespers Magnificat "gaps" as not gaps
Deploy / deploy (push) Successful in 1m33s
Live-checked both instead of leaving them as unauthored: the Vigil of
Christmas never has its own Vespers in the real rite (Dec 23 evening is
plain Advent-4 ferial Vespers, not First Vespers of the Vigil; Dec 24
evening is always Christmas's own), and marian-saturday's Commune file
has no Vespers antiphon of its own either, because a Saturday's Vespers
is always structurally ceded to the following Sunday. Neither is missing
content to author -- there's no historical text for either case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 18:38:08 -04:00
will 04a8686551 Finish the last 5 vigils: weekday-keyed commemoration antiphons
Deploy / deploy (push) Successful in 1m32s
Corrected an earlier "no source exists" claim by live-querying real
commemorated instances of the Vigils of St. Lawrence and the Assumption
across six different weekdays. Their source files' own [Rule] block
("Versum Feria") turns out to be a real, sourced mechanism, not an
absence: a mere commemoration doesn't reuse the vigil's own antiphon at
all -- it borrows the plain ferial day's antiphon/versicle instead, and
that antiphon quotes the Benedictus canticle in weekday sequence (same
shape vespers-magnificat-antiphons.yml already uses for the Magnificat).
The versicle is a single fixed text. Sunday has no entry: a Vigil
transfers off an ordinary Sunday rather than ever reaching a
commemoration there, confirmed live as well as already true in this
app's own precedence rules.

sanctoralCommemorationPart now takes weekday and resolves this
dynamically for common-of-a-vigil saints, rather than needing a static
per-saint file. All 218 saints with an authored collect now have a real
commemoration antiphon.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 18:12:12 -04:00
will b5f16ceb23 Finish the sanctoral commemoration bundle sweep (213/218 saints)
Deploy / deploy (push) Successful in 1m29s
Follow-up to the earlier bulk pass. Sourced the remaining category
templates: BVM feasts (13, reused the already-verified BVM suffrage
versicle plus each feast's own antiphon), angels (4, versicle inferred
from a saint file's own "structural category" note), Confessor-Doctor/
-Pope (7, resolved to C4a/C5a/C4b -- each inheriting its base category's
Ant2/Versum2 unchanged, no new content needed), several confessors and
apostles-plural (2, reusing already-sourced category versicles), and 5
fully-proper feasts (All Saints x2, Finding of the Holy Cross, Most
Precious Blood, Transfiguration, Our Lady of Ransom) given their own
real Ant2/Versum2 straight from their Sancti files.

213 of ~218 real saints now covered. Only the 5 Vigils remain open --
no dedicated Commune template exists for that category in the reference
engine, and FeastClass 'vigil' rarely reaches the commemoration code
path anyway.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 17:10:34 -04:00
will 2c3f63ef97 Bulk-author sanctoral commemoration antiphon+versicle bundles
Deploy / deploy (push) Successful in 1m28s
Found that 217 of 220 saints with an authored collect had no
${propers}-commemoration.yml bundle, so sanctoralCommemorationPart was
silently falling back to a bare collect with no antiphon whenever that
saint was merely commemorated rather than winning outright (user-reported
for St. Raymond Nonnatus and the Decollation of St. John the Baptist).

Bulk-fixed: reused each saint's own proper antiphon where authored, else
the standard de-Communi [Ant 2]/[Versum 2] for that saint's own Common
category. Confirmed via three already live-verified commemorations
(Ss. Felix and Adauctus, St. Clare, St. Sabina) that [Ant 2]/[Versum 2]
are the real tags used for a mere commemoration, not [Ant 1]/[Nocturn 2
Versum] as first assumed -- corrected St. Raymond Nonnatus's file
accordingly. 182 of ~218 real saints now covered; entries not
independently live-verified are marked draft. Remaining ~36 (BVM feasts,
angels, confessor-doctors/popes, vigils) tracked in TODO.md.

New tests/propers/commemoration-sweep.test.ts (182 cases) asserts every
bundle has a real antiphon+versicle+collect, never bare/missing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 16:42:20 -04:00
will e8463f25b2 Give St. Raymond Nonnatus and the Decollation their own commemoration antiphons
Deploy / deploy (push) Successful in 1m30s
Same bare-collect-only gap as Ss. Felix and Adauctus: both saints were
falling back to a plain collect with no antiphon when merely commemorated
(today's and yesterday's Vespers respectively). Raymond has no proper
antiphon of his own, so uses the standard Common-of-a-Confessor-Not-Bishop
text; the Decollation reuses his own already-authored proper Lauds
antiphon, paired with the generic Common-of-a-Martyr versicle. Both
live/reference-verified.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 15:55:15 -04:00
will 6334b3eaaa Give Ss. Felix and Adauctus a real commemoration antiphon
Deploy / deploy (push) Successful in 1m29s
Their commemoration was falling back to a bare collect (only
ss-felix-and-adauctus-collect.yml existed, no combined Ant+V/R bundle),
so it rendered with no antiphon at Lauds when they're commemorated on a
Sunday (today, 2026-08-30). Live-verified against that exact date.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 08:59:22 -04:00
will a18ad7f57c Resolve Advent IV landing on Dec 24 as the Vigil, with Advent IV commemorated
Deploy / deploy (push) Successful in 1m25s
When Advent 4's own Sunday lands on Dec 24, the Vigil of Christmas's
identity (rank, collect, Lauds antiphons, Matins Gospel nocturn) now
governs the day, matching the reference engine's own rule that a
privileged Sunday never displaces it. Unlike the reference engine's own
"no commemoratio" for this exact collision, Advent 4 is deliberately
still commemorated — the Sundays of Advent have real standing of their
own. Matins' three-nocturn Sunday psalmody is untouched either way,
since it's driven by weekday, not by which id wins.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 08:55:00 -04:00
will 654ec868cb Give the Vigil of Christmas (Dec 24) its own proper content
Deploy / deploy (push) Successful in 1m28s
Non-Sunday Dec 24 previously just inherited Advent 4's ferial collect and
antiphons with nothing of its own. Import the Vigil's real proper content
from the reference engine (Duplex I classis): its own collect, all 5 Lauds
antiphons, the Benedictus antiphon, and Matins' Gospel (Matt 1:18-21) +
St. Jerome homily, wired via a new `vigil-of-christmas` temporal id scoped
to non-Sunday Dec 24 only. Advent IV landing on Dec 24 itself is untouched,
left as its own separate design question.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 07:45:44 -04:00
will b3a7176cb8 Add the O Antiphons (Dec 17-23) as an unconditional Vespers Magnificat override
Deploy / deploy (push) Successful in 1m27s
advent-4's own remaining Magnificat-antiphon gap turned out to be a
mechanism problem, not missing content: Dec 17-23 always carries one of
the seven "O Antiphons" at Vespers, keyed to the calendar date itself,
not to whichever Advent week (advent-3 or advent-4, varies by year)
happens to govern that date -- a single per-week file was structurally
the wrong shape.

Added vespers-o-antiphons.yml (MM-DD keyed) and a new lookup tier in
getMagnificatAntiphon, checked unconditionally ahead of both the
per-week temporal file and a real sanctoral winner's own antiphon --
direct instruction: the O Antiphon wins this one text slot on these
seven dates regardless of who otherwise wins the day (e.g. St. Thomas,
Dec 21, still keeps every other part of his own office). Two of the
seven (O Clavis David/O Oriens, Dec 20-21) couldn't be live-verified --
every reference-engine track routes those two dates through St.
Thomas's Vigil/feast instead, which this app's own vigil-of-st-thomas.yml
already documents diverging from -- so they're marked draft rather than
verified.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 07:04:30 -04:00
will 66bd970fed Author the missing Vespers Magnificat antiphon for 52 temporal ids
Every temporal id had its Lauds (Benedictus) antiphon authored, but only
3 of 57 had the Vespers (Magnificat) counterpart -- a whole-calendar
gap exposed by 2026-08-29's Vespers rendering with no Magnificat
antiphon at all. Sourced from the reference engine's static Tempora/
Sancti files, same convention and per-id reference dates already
established for the Benedictus antiphons.

advent-4 and marian-saturday are the only two ids still missing one
(neither resolves from the static files alone) -- follow-up commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 07:03:52 -04:00
will 5ae1f3a3e0 Fix Vespers status wrongly saying "Second Vespers" when a Sunday anticipates it
Deploy / deploy (push) Successful in 1m29s
getVespersStatusLabel detected First-Vespers anticipation only via
winner.vespersFrom, a tag only ever set for a sanctoral tomorrow-winner
(tagVespersFrom). When Vespers anticipates a Sunday that wins its own
day outright (a temporal winner, e.g. 2026-08-29 anticipating 2026-08-30
since the Decollation of St. John Baptist is below the duplex-2-classis
floor that would let it keep its own evening), the tag never gets set
and the status wrongly read "Second Vespers (of today)" even though the
whole page was showing tomorrow's identity.

Comparing the resolved day's date against the originally-requested date
detects anticipation uniformly regardless of winner kind, so this drops
the vespersFrom dependency entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 06:10:52 -04:00
will 95901ef56a Always list the day's actual winner first in getDayLabel
The anchor-day and plain-temporal-fallback branches were listing any
commemorated saint before the winning identity, so a lower-ranked saint
merely riding along as a commemoration (e.g. St. Andrew on Advent I, or
Ss. Tryphon/Respicius/Nympha on an ordinary Sunday) read as if it were
the day's own winner, with the actual winner's rank label dangling at
the end with no name attached. The winner should always lead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ME8QNPdEmHSbSx1r6VmDRG
2026-08-30 06:10:14 -04:00
will 5e56611944 Give St. Sabina her Lauds commemoration antiphon and correct collect
Deploy / deploy (push) Successful in 1m25s
She was rendering as a bare collect (propers: null had no
sanctoral-commemoration fallback), and even that collect used
collect-c6's Virgin-and-Martyr text -- she was a matron, not a virgin.
Live-verified against Divino Afflatu: added collect-c7 (the Common's
own non-Virgin-Martyr template) and a full antiphon/versicle/collect
commemoration bundle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPkfGLiq58pmmDoBWH5UX
2026-08-29 11:06:57 -04:00
will 7393b3e2be Drop the redundant (Exodus)/(Deuteronomy) parenthetical from canticle names
Deploy / deploy (push) Successful in 1m27s
Both Canticle of Moses entries carried a book-name parenthetical in
their display name, redundant with (and now, since the last commit,
duplicated verbatim by) the citation shown right underneath. Only one
canticle is ever displayed per day, so there's nothing to disambiguate
in the UI -- trim to plain "Canticle of Moses" for both.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPkfGLiq58pmmDoBWH5UX
2026-08-29 10:40:13 -04:00
will 4e1266d4ae Give each half of Saturday's split Deuteronomy canticle its own citation
Deploy / deploy (push) Successful in 1m27s
canticleText() always returned the whole canticle's stored citation
("Deut 32:1-65"), even when only slicing out half its verses -- both
Saturday Lauds pieces showed the same full range regardless of which
half was actually being said. Derive the citation from the slice's own
first/last verse numbers instead, only when slicing; a full canticle's
stored citation is left as-is, since it already matches what's shown
and a formula can't reproduce an irregular one like
canticum-trium-puerorum's "Dan 3:57-88,56" (a non-contiguous liturgical
splice).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPkfGLiq58pmmDoBWH5UX
2026-08-29 10:37:39 -04:00
will 7153a846c2 Strip the editorializing parenthetical out of the Canticle of Moses's citation
Deploy / deploy (push) Successful in 1m26s
The user-facing citation field carried an explanatory aside
("liturgical division; split across two Lauds slots") instead of a
plain reference. Move that explanation into the file's own header
comment (which already existed) and leave citation as just "Deut
32:1-65".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPkfGLiq58pmmDoBWH5UX
2026-08-29 10:32:39 -04:00
will d77c9ca68f Add regression tests for Decollation's ferial-numbers/proper-antiphons split
Deploy / deploy (push) Successful in 1m23s
Confirms 2026-08-29 Lauds and 2026-08-28 First Vespers both keep the
ferial psalm numbers (Duplex, below duplex-majus) while still layering
on the feast's own proper antiphons -- and that an override group with
no matching ferial slot (Puellae saltanti / Arguebat) is dropped, not
substituted in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPkfGLiq58pmmDoBWH5UX
2026-08-29 10:27:39 -04:00
will 631d133b16 Apply the same psalm-number/antiphon split to Vespers' psalmody override
Deploy / deploy (push) Successful in 1m24s
Mirrors the Lauds fix: a proper override's psalm numbers stay gated at
duplex-majus+, while its antiphons still layer onto the ferial weekday's
own numbers below that rank, paired by group index.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPkfGLiq58pmmDoBWH5UX
2026-08-29 10:23:33 -04:00
will f733ffa71f Re-gate Lauds proper psalm numbers behind duplex-majus+, keep antiphons rank-agnostic
A feast's own proper psalm-group antiphons should surface at any rank
(f3cf30c), but the actual psalm numbers sung should only substitute at
duplex-majus+, per real Tridentine/Monastic rubric — a lower-rank feast
keeps the ferial psalm cycle and only gets proper text layered onto it.
Below duplex-majus, merge the proper override's antiphons onto the
ferial weekday's own psalm numbers/canticle, paired by group index.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPkfGLiq58pmmDoBWH5UX
2026-08-29 10:23:29 -04:00
will 949196b847 Author proper Lauds/Vespers antiphons for the Beheading of St. John Baptist
Deploy / deploy (push) Successful in 1m32s
Live-verified against Divinum Officium (Tridentine 1906/1910 and
Divino Afflatu 1954, Sancti/08-29.txt) — the feast's own 5-antiphon
set ([Ant Vespera], reused verbatim at Lauds as [Ant Laudes]), now
wired through both the newly rank-agnostic Lauds/Vespers proper tier
and the new minor-hour derivation mechanism. Vespers gets only the
first four (mapped onto vu's Monastic-1617-structured four-psalm
Vespers, matching common-of-a-martyr.yml's own 109/111/112/115
baseline) — the reference's own fifth antiphon is a secular five-
psalm-Vespers detail that doesn't apply here, and is used only at
Lauds' Laudate psalm instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFGHb4XMe6Wya4pGpPhEEi
2026-08-29 07:35:33 -04:00
will 74ddd61aee Derive Prime/Terce/Sext/None antiphons from a saint's own Lauds set
resolveMinorHourAntiphon fell straight from a (rarely-authored)
dedicated per-hour proper file to the saint's generic Common-category
antiphon, skipping the real proper text a saint may already have as
its 5-antiphon Lauds/Vespers set. The reference engine's own
getanthoras() mechanism derives exactly these four minor-hour
antiphons from that same 5-antiphon set by fixed index (Prime/Terce/
Sext/None <- antiphons 1/2/3/5) whenever no dedicated per-hour proper
exists; vu had no equivalent, so any minor hour fell back to the
generic Common antiphon even when the feast's own text was already
sitting right there in its Lauds override.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFGHb4XMe6Wya4pGpPhEEi
2026-08-29 07:35:22 -04:00
will f3cf30c1c7 Stop gating Lauds/Vespers proper psalmody antiphons behind rank
getPsalmodyOverrideFor bundled "which psalm numbers are said" and
"which antiphons are used" into one lookup, both gated behind the same
duplex-majus+ rank threshold. A saint's own authored proper antiphon
text should never be gated behind rank — only the weaker, generic
Common-category substitute has any business being rank-gated. Splits
the lookup into two independent tiers: the feast's own proper (any
rank, via the new getPsalmodyProperOverrideId) tried first, then the
existing duplex-majus+ Common-category fallback — mirroring the split
getMinorHourOverrideId already made for the chapter/responsory/hymn/
versicle bundle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFGHb4XMe6Wya4pGpPhEEi
2026-08-29 07:34:48 -04:00
will b163f502b8 Scope renderCitation to the currently displayed languages
renderCitation deduplicated a text's citation values across every
stored language, not just the ones actually being rendered. Once a
citation's La/En abbreviations genuinely differ (e.g. "Jac 1:12" vs.
"Jas 1:12", or "Sap. 10:10" vs. "Wis. 10:10"), both survived the
dedupe and got joined together regardless of view mode, leaking the
other language's abbreviation into a single-language display.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFGHb4XMe6Wya4pGpPhEEi
2026-08-29 07:33:48 -04:00
will 7ab517cbe3 Fall back to a saint's collectCommon/collectName in commemorations too
sanctoralCommemorationPart() only ever checked a commemorated saint's
own propers, so a saint like St. Sabina — no propers of her own, but
a real collectCommon + collectName template fallback, verified text
and all — rendered as an outright "missing" commemoration. The day's
primary-collect resolver (getDayCollect) already had this same
collectCommon/collectName fallback tier; this brings the
commemoration path in line with it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFGHb4XMe6Wya4pGpPhEEi
2026-08-29 07:33:30 -04:00
will a3a9d568ed Include the canticle's own citation in Lauds' resolved output
canticleText() built the resolved text/status from the weekday
canticle's verses but never copied the source YAML's own citation
field across, so every Lauds canticle rendered with no book/chapter/
verse citation at all despite the data already carrying one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFGHb4XMe6Wya4pGpPhEEi
2026-08-29 07:33:15 -04:00
will a6e4bb15b1 Author real proper Matins/Vespers content for the Nativity of St. John the Baptist
Deploy / deploy (push) Successful in 1m30s
nativity-of-st-john-the-baptist.yml's `common` field (common-of-an-apostle)
was the same mistaken placeholder as the Beheading's, but this feast
needed a different fix: the reference engine assigns it no Commune at
all -- every [Rule] line in Sancti/06-24.txt and SanctiM/06-24.txt points
to proper text, never a Commune category -- so there's no better-fitting
id to swap in either.

Authored the real proper content that was still falling through to the
apostle Common, live-verified against the local Divinum Officium CGI
instance (Monastic Tridentinum 1617):

- Matins invitatory antiphon ("Regem Præcursóris Dóminum")
- Matins hymn ("Antra desérti téneris," with the Monastic doxology
  substitution already used by this feast's own Lauds/Vespers hymns)
- Full Matins Nocturn 1-3 psalmody + nocturn versicles (Nocturn 1/2 reuse
  St. Lawrence's own shared psalm-number pool per "Psalmi Dominica";
  Nocturn 3 reuses St. Lawrence's own canticle scripture files verbatim,
  since the citations are identical)
- Vespers psalmody (only 4 psalm slots at this Monastic rank, not 5 as a
  naive reading of the raw Ant Vespera list would suggest)
- Nocturn 2/3 responsories on the existing nocturn-readings file

`common: common-of-an-apostle` is left in place as a fully inert
placeholder (a non-optional field with no better id available) with an
updated comment explaining why.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvnBMq7HGfXRyMsVLu2v4D
2026-08-29 06:24:33 -04:00
will d4ec104d59 Fix Beheading of St. John the Baptist's wrong Common category
decollation-of-st-john-baptist.yml's `common` field was set to
common-of-an-apostle -- a mistaken placeholder (he isn't an apostle) that
leaked into the Aug 29 Matins invitatory and the Lauds/Vespers office
bundle. Corrected to common-of-a-martyr, matching the file's own already-
correct `minorHoursCommon` field.

Also corrected a stale comment in the feast's own nocturn-readings file
claiming neither Nocturn 2 nor 3 has a proper responsory: Nocturn 2's own
closing responsory is genuine proper text (not a Commune reference like
Nocturn 3's), and is now authored.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvnBMq7HGfXRyMsVLu2v4D
2026-08-29 06:22:49 -04:00
will a2d082e608 Keep a short litany + Our Father at Compline on a Double, don't omit
Deploy / deploy (push) Successful in 1m28s
compline-preces.yml was already the secular Tridentine 1906/1910 text
word-for-word (verified live against both tracks), so no text change
was needed there. The actual gap: both Monastic and secular tracks drop
Compline's Preces to nothing on a Double-or-higher, unlike Vespers/Lauds
which fall back to a short form. Per explicit request, this is now a
deliberate vu-specific deviation from both tracks: on a Double, Compline
substitutes the same short Kyrie/Pater-Noster litany Vespers already
falls back to on Sundays/feasts, instead of showing nothing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHXJAWVAabCKX1JgmDh4Rn
2026-08-29 05:29:56 -04:00
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
will 33c9b2415b 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
2026-08-28 20:51:06 -04:00
will 25934865b1 Model a Matins Gospel pericope and its paired homily as one atomic part
Deploy / deploy (push) Successful in 1m25s
Replaces the lesson+isGospel flag with a dedicated 'gospel' ResolvedPart
carrying an optional nested homily. Previously a Gospel pericope and its
patristic homily were two separate pool entries, which the front-light
1/1/rest nocturn slicer could split into different nocturns purely by
index. buildReadingPool now folds an isGospel:true reading and its
immediately-following same-nocturn homily into a single pool entry, so
the pairing is structural rather than a convention the slicer has to
honor. hour-view.ts renders the pair as one visually grouped section.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1LqodVbQdQVoTGPz9sSw4
2026-08-28 19:46:48 -04:00
will 6a17a74b9b Fix Vespers ferial psalmody following an anticipated feast's identity
When tonight anticipates tomorrow's First Vespers, resolvePsalmody was
keying off the borrowed day's weekday instead of today's actual calendar
weekday, silently shifting the fixed 6-day psalm rotation. The psalm
cycle has no per-feast override and must stay on today's own place in
the rotation regardless of whose office governs the evening's propers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1LqodVbQdQVoTGPz9sSw4
2026-08-28 19:46:33 -04:00
will 4fdddbf4a2 Drop remaining done-status aside from Matins sanctoral-readings bullet
Deploy / deploy (push) Successful in 1m11s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1LqodVbQdQVoTGPz9sSw4
2026-08-28 19:26:55 -04:00
will de87aa74ba Trim done-content detail out of Minimal-proof-done bullets
Deploy / deploy (push) Successful in 1m35s
The Matins bullets repeated a lot of already-done detail inline; that
detail already lives in its own dated build-log sections. Left concise
pointers to those sections and kept only the actual remaining gap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1LqodVbQdQVoTGPz9sSw4
2026-08-28 19:25:38 -04:00
will bbbdc6c347 Remove invented "Recently closed items" heading
Deploy / deploy (push) Successful in 1m25s
Those entries belong in the same running dated build log as everything
else in this section — no separate heading was needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1LqodVbQdQVoTGPz9sSw4
2026-08-28 19:18:09 -04:00
will 0142d7349b Merge the two Known-simplifications sections into one, near the top
Deploy / deploy (push) Successful in 1m28s
The priority-list stub pointed at a separate list of the same name near
the end of the file; consolidated into one concise section under item 3
and tightened the wording. Renamed the now-emptied end-of-file heading
to "Recently closed items" to accurately describe the done entries still
parked there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1LqodVbQdQVoTGPz9sSw4
2026-08-28 19:13:53 -04:00
will 725226dcea Move fully-done items out of Minimal-proof-done section into build log
Deploy / deploy (push) Successful in 1m26s
Matins psalmody overrides (all 19 categories), the Commemoratio Octavae/
fixed Lord's-BVM feast exclusions, and the Lauds/Vespers bundle were
completely finished with no open remainder, but still sat in the
"bulk content pending" section. Gave them their own dated done sections
so that section only lists genuinely partial work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1LqodVbQdQVoTGPz9sSw4
2026-08-28 19:09:20 -04:00
will be4f872b34 Move completed items out of Mechanism gaps into the dated build log
Deploy / deploy (push) Successful in 1m24s
St. Hermes removal, psalm-list accessibility fix, Te decet laus, and the
ferial Preces psalm drops were all done but still listed under the open
Mechanism gaps section; moved to their own dated done entries so that
section only holds actually-open work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1LqodVbQdQVoTGPz9sSw4
2026-08-28 19:04:57 -04:00
will 71ac6b2672 Drop stale Gospel-redisplay note now that pericope+homily pair is planned
Deploy / deploy (push) Successful in 1m23s
The Te decet laus entry flagged the historical Gospel-read-again-in-full
step as unmodeled; that's moot now that Nocturn 3 will display the Gospel
pericope alongside its patristic homily.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1LqodVbQdQVoTGPz9sSw4
2026-08-28 19:02:27 -04:00
will a5e9f85fdc Clarify Gospel-pericope/homily rendering as a decided design, not open
Deploy / deploy (push) Successful in 1m26s
The pairing of a Nocturn 3 Gospel pericope with its patristic homily
should render as two distinct, visually-distinguished parts rather than
one concatenated lesson; bible-plan Gospel readings stay pericope-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1LqodVbQdQVoTGPz9sSw4
2026-08-28 19:00:07 -04:00
will bdc2058a73 Move Prime ferial Preces gating from open work to settled build log
Deploy / deploy (push) Successful in 1m25s
The generous ferial Preces gating was already a confirmed, final decision
(2026-08-25), not an open question — it shouldn't have been listed under
Open work at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1LqodVbQdQVoTGPz9sSw4
2026-08-28 18:51:44 -04:00
will 9cd59dae03 Render psalm verses as plain divs instead of an <ol>/<li> list
Deploy / deploy (push) Successful in 1m24s
Screen readers announced "list, N items" for continuous liturgical text
that isn't conceptually a list; styling is class-based so no CSS changes
were needed.
2026-08-28 18:40:51 -04:00
will 17112c06a8 Make the Today link context-sensitive to time of day
Deploy / deploy (push) Successful in 1m26s
When already viewing today, clicking Today now snaps the selected hour
to whatever hour would be default for the current wall-clock time
(same as a fresh visit to /vu); when viewing another date it still just
jumps to today while preserving the current hour, as before. The link's
href is built from the same todayTargetHour() decision as the click
handler, so right-click -> open in new tab lands on the same target
instead of following a stale href.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbHcmjnqQPJZ1i2yoebmp5
2026-08-28 16:52:09 -04:00
will 03d164f174 Drop St. Hermes from 8/28 sanctoral calendar
Deploy / deploy (push) Successful in 1m24s
He always lost to St. Augustine and, per the generous-commemoration
design, only ever surfaced as a commemoration, so removing him from
the date list suppresses him entirely. st-hermes.yml and its collect
are left in place, restorable by re-adding the list entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVigobgkuEayoGdEiubBdt
2026-08-28 12:46:05 -04:00
will b19f88696f Rotate Terce/Sext/None psalm assignments for Wed/Thu/Fri/Sat
Deploy / deploy (push) Successful in 1m24s
Wednesday and Friday now carry their own distinct psalm sets (formerly
Friday's and Saturday's, respectively); Tuesday, Thursday, and Saturday
share the gradual psalm set.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVigobgkuEayoGdEiubBdt
2026-08-28 12:41:41 -04:00
will c25505cdc0 Add the Patronage of St. Joseph as a new movable feast with octave
Deploy / deploy (push) Successful in 1m21s
This Duplex I classis feast (instituted by Pius IX, 1847) didn't exist
anywhere in vu's calendar. Dated per the older, pre-1955 reckoning --
Wednesday after the 2nd Sunday after Easter (Easter+17) -- rather than the
1913 Pius X reform's later placement on the 3rd Sunday after Easter
itself, per direct instruction. Mechanism: one new temporal-feast record
plus a new EASTER_OFFSET_STARTS table entry (the only code change --
everything else is the existing generic anchor-driven resolution).

Authored the feast day's own Matins reading and its full 7-day octave
(days 2/3/4/6/7/8; day 5, the Sunday, has no override in the source at
all, correctly left unauthored).

Fixed one real collateral effect: an existing fixed-year calendar test
(St. Pudentiana vs. St. Peter Celestine, 2038-05-19) broke because that
date now falls on this octave's own closing day, tying Celestine's rank
against the octave's threshold -- the app's own documented tie-break
correctly favors the octave that year. Moved the proof date to 2033.
Also bumped the PWA precache file-size cap (6 MiB -> 8 MiB) since this
content pull pushed the main bundle past the previous limit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-28 07:12:08 -04:00
will 283bb356d7 Author Matins readings for the three Christmas-week saint octaves
Deploy / deploy (push) Successful in 1m25s
St. Stephen, St. John the Apostle, and Holy Innocents each have an 8-day
octave overlapping Christmas week, but their intervening days (2-7) carry
no distinct Lectio content in the source -- every suffix file checked at
Dec 27-31 is a bare @Sancti/MM-DD alias, correctly left unauthored, same
pattern as the fixed-date octaves. Only each octave's own closing "In
Octava" day (Jan 2/3/4 respectively) carries real content: St. Augustine's
sermon on Stephen + St. Jerome on Matt 23:34-35; St. Augustine's Tractate
36 on John + St. John Chrysostom on John 21:20-24; St. Augustine's first
sermon for Childermas + St. John Chrysostom on Matt 2:13-18.

This closes the octave-readings backlog entirely -- every fixed-date,
movable-feast, and Christmas-week saint octave has now been audited by
calendar date against the source.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-28 06:49:15 -04:00
will ca5ee5426a Author Matins readings for the three movable-feast octaves
Deploy / deploy (push) Successful in 1m24s
Ascension, Corpus Christi, and Sacred Heart octave content lives in the
reference engine's Tempora/ directory, keyed by liturgical offset from
Easter rather than calendar date. All three octaves' 7 non-feast days
(day 2 through the closing day) turned out to have genuine, distinct
patristic content -- unlike the fixed-date octaves, none were bare
vide-only days. 21 files authored across the three octaves.

Found and fixed one duplication along the way: each octave's own Sunday
day turned out to duplicate content already authored under a different id
in the earlier post-Pentecost temporal-cycle sweep, since a numbered
post-Pentecost Sunday and the Sunday within Corpus Christi's or Sacred
Heart's octave are structurally the same calendar day every year. Both
duplicate files (corpus-christi-octave-day-4, sacred-heart-octave-day-3)
were dropped after confirming the reading still surfaces via the existing
temporal id.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-28 06:40:01 -04:00
will f76c0837bd Author St. Felix I Matins reading; close two fixed-octave-day gaps
Re-auditing the two prior sanctoral bulk passes (pre-1955 Tridentine track
and SanctiM Monastic-1617 track) turned up one saint missed by both:
St. Felix I (May 30), authored the same single-lesson shape as
st-pontian.yml. This closes the saint-level side of the Matins
patristic/hagiographic reading backlog.

Separately, auditing octave-day content by calendar date (the reference
engine keys it that way, not by octave-day-number) found two real gaps in
already-partially-authored fixed-date octaves: Immaculate Conception's own
closing day (Dec 15, Pius IX bull + St. Epiphanius homily) and Nativity of
the BVM's day 3 (Sep 10, St. Jerome on Matthew's genealogy -- no usable
reference English, own translation used, draft). Every other apparent gap
in these octaves was confirmed a correctly-empty vide-only day, not a
further gap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-28 06:39:34 -04:00
will 709bf176f7 Add TODO items: drop St. Hermes commemoration, fix psalm-verse list a11y
Deploy / deploy (push) Successful in 1m23s
Two content/mechanism gaps flagged by the user: St. Hermes (8/28) always
loses to St. Augustine and only ever surfaces as an unwanted commemoration
(fix is a one-line sanctoral-calendar.yml edit, data files untouched); and
psalm verses render as a real <ol>/<li> list, which screen readers announce
disruptively per-verse for content that isn't conceptually a list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XdfjdTvfwgxyaC8BUTEvWy
2026-08-28 06:28:36 -04:00
will f735c8c71d Update TODO.md: all Matins psalmody-override Common categories done
Deploy / deploy (push) Successful in 1m23s
Closes the "Matins psalmody overrides" open-work item entirely -- all
20 Common categories (17 authored/aliased in the category sweep, plus
proper saint-level content for the 5 angel feasts and St. Scholastica)
now resolve real Matins psalmody instead of the plain ferial fallback.
Records the fourth pass's secular-track method for Widow/Several Women
Martyrs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-28 05:40:48 -04:00
will 908f7c320d Author Matins Common-of-a-Widow psalmody (secular-sourced), alias Several-Women-Martyrs
Closes the last 2 of 20 Matins psalmody-override Common categories. The
Monastic-1617 track alone was a genuine dead end here -- no real saint
in the Monastic corpus ever invokes this Common, so there was no live
Monastic rendering to verify a hand-resolution against. Resolved per
explicit user authorization to source secular-track content directly,
consistent with this project's existing blended-source design: combined
a direct read of CommuneM/C7.txt's own psalm-substitution rule with one
live secular query (version=Tridentine - 1906) against St. Bridget of
Sweden to supply the actual antiphon text.

Nocturn 1/2 end up near-identical to common-of-a-virgin-martyr.yml (one
antiphon substituted, since Virgin-Martyr's own text literally calls her
a Virgin) -- too close to alias, not close enough to merge, same
reasoning as common-of-a-confessor vs. common-of-a-confessor-bishop.
Nocturn 3 is genuinely Widow-specific (Parable of the Pearl of Great
Price); its English has no source counterpart and is self-translated,
marked draft-quality.

common-of-several-women-martyrs aliased here: its own Monastic template
whole-file-inherits the same base with no genuine Matins-nocturn
override anywhere in the traced redirect chain.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-28 05:40:43 -04:00
will 39f30b42c1 Update TODO.md for third Matins psalmody overrides pass
Deploy / deploy (push) Successful in 1m24s
Records the Virgin/Confessor-Doctor/Confessor-Pope closures via the
source-file-first method, St. Scholastica's proper-content case, and
corrects a prior pass's wrong claim that Ss. Perpetua and Felicity are
"only ever Commemoratio" -- their own source file shows rank Duplex.
Widow and Several Women Martyrs remain blocked for a sharper reason:
CommuneM/C7a.txt and C7b.txt are synthetic patchworks that no real
saint anywhere in the Monastic corpus ever actually invokes, so there's
no live rendering to verify a hand-resolution against.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-28 05:20:19 -04:00
will 09cf0c1aa0 Alias Virgin/Confessor-Doctor/Confessor-Pope to existing Matins Common schemes
Read the reference engine's CommuneM/*.txt templates directly (C6a,
C4a, C4b), rather than trial-and-error live queries, to confirm each
whole-file-inherits its parent Common's Matins nocturn psalmody with no
override. Live-verified byte-identical against St. Scholastica (Virgin
-> Virgin-Martyr) and St. Ambrose (Confessor-Doctor -> Confessor-Bishop);
Confessor-Pope -> Confessor-Bishop aliased on the source redirect chain
alone, since none of vu's three Pope-Confessor saints exist on the
Monastic-1617 calendar at all (matching the convention their own files
already use for every other hour).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-28 05:20:13 -04:00
will 9e0c1b3eb1 Author St. Scholastica's proper Matins psalmody
Her psalm numbers and all three nocturn versicles match Common-of-a-
Virgin(-Martyr) exactly, but her page is headed "ex Proprio Sanctorum"
and her antiphons genuinely narrate her own rainstorm-miracle episode --
real proper content, not Common text, so she gets her own saint-level
file rather than being left to the Common fallback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-28 05:20:06 -04:00
will 87d28a14bd Fix St. Gabriel's Lauds/Vespers capitulum copy-paste bug
Deploy / deploy (push) Successful in 1m31s
lauds-capitulum-st-gabriel-archangel.yml carried Apparition of St.
Michael's own capitulum ("Significávit Deus...") under Gabriel's id --
found in passing while authoring his Matins psalmody. His real
capitulum, read directly from the reference engine's Sancti/03-24.txt
([Capitulum Laudes]), is Daniel's own account of the angel's visitation
(Dan 9:21-22), genuinely proper to Gabriel, not shared with Apparition
despite sharing the same hymn. No separate vespers-capitulum file exists
for this id, so the fix covers both hours via vespersCapitulumForOverride.

Updated tests/hours/lauds.test.ts: removed Gabriel from the shared-
chapter assertion loop (Guardian Angels/Raphael still legitimately share
Apparition's chapter, confirmed via their own saint files' explicit
source cross-references) and added a dedicated test for his own chapter.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-28 04:31:47 -04:00
will 0725019e67 Update TODO.md for angel feasts' Matins psalmody pass
Deploy / deploy (push) Successful in 1m19s
Closes the Angel item out of the priority list, corrects the prior
pass's wrong claim that Guardian Angels is gated behind a
Monastic Divino 1930 rubric (verified via a resolveOrdo smoke test with
zero missing parts under plain Monastic 1617), and flags a separate
found-in-passing bug: lauds-capitulum-st-gabriel-archangel.yml carries
Apparition's capitulum under Gabriel's id -- left for its own pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-27 22:23:30 -04:00
will fd5ab0781e Author proper Matins psalmody for the 5 angel feasts
Common-of-an-Angel doesn't exist in the reference engine -- every angel
feast has genuine proper Matins content of its own, so these are
saint-level overrides, not a Common category. Transcribed directly from
the reference engine's source files (Sancti/05-08 etc.) after a live-CGI
attempt mis-extracted a mid-nocturn antiphon change.

St. Michael needed his own file: the psalmody-override lookup keys off
the day's resolved winner id, not the `propers` field, and his own
Matins antiphons/lessons turn out to genuinely differ from Apparition's
despite his calendar file's "word-for-word identical" claim (true only
of the collect).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-27 22:23:24 -04:00
will 797ebceafd Update TODO.md for second Matins psalmody overrides pass
Deploy / deploy (push) Successful in 1m22s
Records the Holy-Woman/Several-Confessors/Evangelist closures, the
Angel non-gap finding (no such Common exists in the reference engine --
every angel feast has genuine proper content instead, a different task),
and sharper blockers for the remaining Virgin/Widow/Confessor-Doctor/
Confessor-Pope/Several-Women-Martyrs categories.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-27 22:09:18 -04:00
will b7c722078d Alias Several-Confessors/Evangelist to existing Matins Common schemes
Live-verified byte-identical against Ss. John of God and Camillus de
Lellis (Several Confessors -> Common-of-a-Confessor) and St. Luke
(Evangelist -> Common-of-an-Apostle) -- genuine full redirects, matching
the reference engine's own votive-index adjacency (C1/C1a, C5/C5c).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-27 22:09:13 -04:00
will 3142506bd0 Author Matins Common-of-a-Holy-Woman psalmody
Live-verified against St. Anne (2026-07-26): Nocturns 1/2 and Nocturn
3's canticles/versicle match Common-of-a-Virgin-Martyr exactly, but the
Nocturn 3 antiphon itself differs (pearl-of-great-price vs. bridegroom
imagery), so it gets its own file rather than an alias.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-27 22:09:08 -04:00
will 6d983dfc73 Update TODO.md for Matins psalmody overrides pass
Deploy / deploy (push) Successful in 1m23s
Records the nine-category/three-alias progress, the votive-parameter
dead end for Matins nocturn psalmody, and the specific blockers found
for each of the eight still-open Common categories.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-27 21:48:07 -04:00
will a003d01ed0 Alias Abbot/Confessor-not-Bishop/Apostles-plural to existing Matins Common schemes
Live-verified byte-identical psalms, antiphons, and versicles against
St. Maurus/St. Gall (Abbot), St. Francis of Assisi/St. Jerome
(Confessor-not-Bishop), and the Octave of Ss. Peter and Paul
(Apostles-plural) — genuine full redirects, not new content. Swaps the
matins.test.ts ferial-fallback proof off St. Anthony Abbot (no longer
unauthored) onto St. Monica (Widow, still unauthored).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-27 21:48:02 -04:00
will a85f113097 Author Matins Common-of-Virgin-Martyr/BVM/Dedication psalmody
Live-verified against Divinum Officium (Monastic Tridentinum 1617) via
clean representative saints with no proper content of their own:
St. Justina (Virgin-Martyr), Our Lady of the Snows (BVM), and the
Dedication of the Lateran Basilica.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhysogLP9eCeKS7JEwRi6y
2026-08-27 21:47:56 -04:00
will 5397e564b3 Fix Psalm 87:11 English translation: physicians -> shades
Deploy / deploy (push) Successful in 1m22s
Douay-Rheims's "physicians" is a literal rendering of the Vulgate's
"medici," itself a mistranslation of Hebrew Rephaim (the shades of
Sheol). Replace with "shades" for clarity, matching modern
translations (RSV/NRSV/NABRE).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018AThxkXu7pbPP7dLBkUpLj
2026-08-27 12:28:43 -04:00
will 366ada1b26 Add name-template substitution for Common-of-a-Holy-Woman's invitatory antiphon
Deploy / deploy (push) Successful in 1m20s
Commune/C7's own invitatory antiphon ("Laudémus Deum nostrum * In
confessióne beátæ N..") needs a per-saint genitive-singular name, unlike
every other Common invitatory antiphon currently authored. Exports
resolve-common.ts's existing substituteName helper (previously only used
by getDayCollect for collectCommon/collectName) and wires it into
resolveMatinsInvitatoryText via a new SaintRecord.invitatoryName field
-- kept separate from collectName since the two templates' {N}
placeholders don't always need the same grammatical case. Fills in real
declined names for the 3 saints using common-of-a-holy-woman (Anne,
Martha, Mary Magdalene).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012AVXS5iTrk3c2anSYVGwxS
2026-08-27 09:26:22 -04:00
will f55cd46272 Author Matins invitatory-antiphon Common tier for Confessors/Martyrs/Virgins/Dedication
Deploy / deploy (push) Successful in 1m23s
resolveMatinsInvitatoryText's Common-category tier existed but only had
Common-of-an-Apostle authored, so every other Common category fell all
the way through to the plain ferial invitatory antiphon regardless of
rank (e.g. St. Joseph Calasanctius, 2026-08-27, Confessor). Adds the
Confessor family (5 duplicated ids, matching the hymn's own precedent),
Martyr family (2 ids + Several Martyrs), Virgin family (2 ids), and
Dedication — all read directly from the reference engine's own
Commune/C-number [Invit] lines. Holy Women (C7) needs a name-template
mechanism this project doesn't have yet for invitatory antiphons, and
General/All Saints (C9) turned out to be a mislabeled backlog item (C9 is
actually the Office of the Dead) — both logged in TODO.md instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012AVXS5iTrk3c2anSYVGwxS
2026-08-27 08:53:33 -04:00
will 63463c696f Author missing Terce/Sext/None/Prime antiphons for common-of-a-confessor(-not-bishop)
Deploy / deploy (push) Successful in 1m21s
common-of-an-abbot's own P/T/S/N antiphon files already claimed to cover
common-of-a-confessor and common-of-a-confessor-not-bishop too ("an Abbot
is himself a Confessor"), but the matching duplicate files were never
authored — so the ~36 saints using either of those two ids (bare
common-of-a-confessor is the more common of the two) got no minor-hour
antiphon at all. Adds the 8 missing files, copying the abbot file's
already-verified text, matching this category's existing duplicate-not-
alias convention (see matins-hymn-common-of-a-confessor.yml vs.
matins-hymn-common-of-a-confessor-not-bishop.yml).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012AVXS5iTrk3c2anSYVGwxS
2026-08-27 08:39:35 -04:00
will bc432222d1 Give plain-Duplex Matins feasts real ferial psalms and their own Terce/Sext/None antiphons
Nocturn count stays rank-based (readings only), but psalm content for an
exactly-Duplex sanctoral winner with no proper now always uses that
weekday's real ferial psalms instead of Common's own psalm numbers (or a
bare mechanical chunk) — so the psalter actually gets prayed rather than
repeating the same dozen Common psalms. Each nocturn's antiphon comes from
the saint's own minorHoursCommon Terce/Sext/None text (real, distinct per
nocturn, already proven non-psalm-specific), with Common's own Matins
versicle per nocturn; falls back to bare only where that content isn't
authored. Duplex-majus+ winners are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012AVXS5iTrk3c2anSYVGwxS
2026-08-27 08:39:26 -04:00
will 8b1c689269 Update TODO.md: close Lauds/Vespers bundle item, log Circumcision hymn fix
Deploy / deploy (push) Successful in 1m22s
The top-of-file priority summary still said 10 Common categories were
open, but the dated build log below it already showed all 11 done and
the Matins hymn gap closed to match, both from the prior session -- the
summary had drifted out of sync. Also logs today's Circumcision hymn fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 16:47:28 -04:00
will 4873c7a36a Author Circumcision's own Matins/Lauds/Vespers hymn
Closes the last content gap flagged when Circumcision was added: its
rubric borrows Christmas Day's propers wholesale, but Christmas Day
itself has no hymn authored anywhere in vu yet. Lauds reuses the
already-verified Christmas Octave Sunday text (byte-identical in the
source); Vespers/Matins use the Monastic "Christe Redemptor omnium",
a genuinely separate hymn from the secular "Jesu, Redemptor omnium",
with a fresh draft English translation since none exists in the
reference engine for that variant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 16:47:21 -04:00
2889 changed files with 204207 additions and 4028 deletions
+3117 -135
View File
File diff suppressed because it is too large Load Diff
+18 -1
View File
@@ -56,8 +56,25 @@ export function setDate(date: string): void {
notify();
}
/**
* Which hour the "Today" link should point to: if we're already viewing
* today, the current time-of-day default (as if freshly visiting `/vu`
* right now); otherwise the hour currently selected, unchanged. Shared by
* `goToToday()` and the link's `href` (`day-nav.ts`) so a plain click and
* a right-click-open-in-new-tab land on the same target.
*/
export function todayTargetHour(): HourId {
return state.date === todayIso() ? defaultHourForTime(new Date()) : state.selectedHour;
}
export function goToToday(): void {
setDate(todayIso());
const today = todayIso();
const hour = todayTargetHour();
if (state.date === today) {
setSelectedHour(hour);
} else {
setDate(today);
}
}
export function addDays(date: string, days: number): string {
+2 -2
View File
@@ -19,11 +19,11 @@ export interface CollisionResult {
}
function asWinner(candidate: SanctoralIdentity): DayWinner {
return { kind: 'sanctoral', id: candidate.id, name: candidate.name, rank: candidate.rank };
return { kind: 'sanctoral', id: candidate.id, name: candidate.name, nameLa: candidate.nameLa, rank: candidate.rank };
}
function asCommemoration(candidate: SanctoralIdentity): Commemoration {
return { kind: 'sanctoral', id: candidate.id, name: candidate.name, rank: candidate.rank };
return { kind: 'sanctoral', id: candidate.id, name: candidate.name, nameLa: candidate.nameLa, rank: candidate.rank };
}
/**
+34 -14
View File
@@ -83,11 +83,11 @@ export interface OccurrenceResult {
}
function sanctoralCommemoration(candidate: SanctoralIdentity): Commemoration {
return { kind: 'sanctoral', id: candidate.id, name: candidate.name, rank: candidate.rank };
return { kind: 'sanctoral', id: candidate.id, name: candidate.name, nameLa: candidate.nameLa, rank: candidate.rank };
}
function sanctoralWinner(candidate: SanctoralIdentity): DayWinner {
return { kind: 'sanctoral', id: candidate.id, name: candidate.name, rank: candidate.rank };
return { kind: 'sanctoral', id: candidate.id, name: candidate.name, nameLa: candidate.nameLa, rank: candidate.rank };
}
/**
@@ -143,11 +143,22 @@ export function decideOccurrence(
};
case 'privileged-feria-major':
// Ash Wednesday, Holy Week, the Easter Octave, the Vigil of
// Christmas — never displaced, same as privileged-sunday.
if (isAtLeast(sanctoral.rank, 'duplex-majus')) {
return { winner: temporalWinner, commemorations: [sanctoralCommemoration(sanctoral)] };
}
// Ash Wednesday, Holy Week, the Easter Octave, Low Sunday, the
// Vigils of Christmas/Pentecost — never displaced, and never gives
// even a bare commemoration to any rank, same as privileged-sunday.
// The "duplex-majus+ still gets a nod" branch this case used to
// have was wrong, not just unverified: the one case that seemed to
// support it (St. Mark, Duplex II. classis, "merely commemorated"
// within the Easter Octave) turns out to have been a misreading —
// live-verified 2026-09-01, direct instruction ("you dont
// commemorate -anything- from palm sunday through low sunday"),
// confirmed with two more dates on top of the Annunciation/Palm-
// Easter-Sunday evidence already in the privileged-sunday case
// below: the Annunciation (Duplex I. classis) transfers with zero
// commemoration on Holy Thursday itself in years it lands there
// (2027/2032/2043), and Chair of St. Peter at Antioch (Duplex
// majus) transfers off Ash Wednesday the same way (2034) — so the
// exception was wrong for Ash Wednesday too, not just Holy Week.
return {
winner: temporalWinner,
commemorations: [],
@@ -188,13 +199,22 @@ export function decideOccurrence(
};
case 'privileged-sunday':
if (isAtLeast(sanctoral.rank, 'duplex-majus')) {
// Never displaced, but a sufficiently high feast still gets a nod.
return { winner: temporalWinner, commemorations: [sanctoralCommemoration(sanctoral)] };
}
// Below duplex-majus, a privileged Sunday wants nothing at all —
// not even the bare commemoration an ordinary Sunday would allow a
// Simplex — so everything here transfers.
// A privileged Sunday wants nothing at all, no matter how high the
// candidate ranks — not even the bare commemoration an ordinary
// Sunday would allow a Simplex, and no exception for duplex-majus+ —
// everything transfers. Live-verified 2026-09-01, three independent
// dates: the Immaculate Conception (Duplex I. classis) transfers off
// Advent II with no commemoration at all (real dates 2024/2030/2041),
// and the Annunciation (also Duplex I. classis) transfers off both
// Palm Sunday and Easter Sunday itself the same way (2029/2035/2040/
// 2046) — confirmed by direct instruction as the correct behavior,
// correcting an earlier, never-actually-verified "duplex-majus+
// still gets a nod" guess. (Where a transferred candidate actually
// lands is a separate, still-generic `transferDirectionOf`/next-
// open-day mechanism — the Annunciation's own real rule of jumping
// *past* the whole Easter Octave rather than landing inside it is a
// known follow-on, blocked on the not-yet-built Easter-octave
// mechanism; see TODO.md.)
return {
winner: temporalWinner,
commemorations: [],
+133 -39
View File
@@ -12,12 +12,15 @@
//
// Bilingual: every label is built as a `Bi` (`{en, la}`) pair throughout.
// Proper names (a saint's name, an octave's name, a named temporal feast's
// name) have no authored Latin form anywhere in `data/` today — those use
// the English string as their own `la` value too (same "surface real text
// rather than block on missing" convention used elsewhere in this app),
// rather than leaving Latin mode showing blanks. Only the closed,
// mechanism-level vocabulary here (weekdays, ranks, season/ordinal
// phrasing) has real Latin authored.
// name) carry an optional `nameLa` field alongside the required English
// `name` — see calendar/types.ts's `SanctoralIdentity.nameLa` doc comment.
// Authored for essentially every saint/octave/named-feast as of 2026-08-31
// (see TODO.md); `biName` still falls back to the English string as the
// `la` value for the rare unauthored case (same "surface real text rather
// than block on missing" convention used elsewhere in this app), rather
// than leaving Latin mode showing blanks. The closed, mechanism-level
// vocabulary here (weekdays, ranks, season/ordinal phrasing) has always
// had real Latin authored, independent of this.
import type { FeastClass, LiturgicalDay, TemporalCategory, Weekday } from './types';
import { easterSunday } from './easter';
import { adventStart, firstSundayStrictlyAfter, sundayOnOrBefore } from './temporal';
@@ -36,16 +39,49 @@ function bi(en: string, la: string): Bi {
return { en, la };
}
/** A proper name with no authored Latin form yet — the English string
* stands in for `la` too. See file-header note. */
function biFallback(name: string): Bi {
return { en: name, la: name };
/** A proper name (saint, octave, named temporal feast) with an optional
* authored Latin form — falls back to the English string when `nameLa`
* hasn't been authored yet, same convention as `biFallback` above. */
function biName(name: string, nameLa: string | undefined): Bi {
return { en: name, la: nameLa ?? name };
}
function joinBi(items: Bi[], sep = ' — '): Bi {
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 {
return text.charAt(0).toUpperCase() + text.slice(1);
}
@@ -348,13 +384,13 @@ function temporalLabel(day: LiturgicalDay): Bi {
* own winner is the feast itself, handled above before this is ever
* called) — kept simple rather than special-cased for that rare edge case
* (see applyOctaves's own `isOwnStartDay` comment in calendar/index.ts for
* when it can happen). The octave's own `name` has no authored Latin form
* (see file header), so both languages currently show the same name text
* — only the surrounding phrasing differs. */
* when it can happen). Falls back to the English name for `la` too when
* the octave's own record has no authored `nameLa` yet (see file header). */
function octaveCoreName(octave: ActiveOctave): Bi {
const name = biName(octave.name, octave.nameLa);
return octave.isClosingDay
? bi(`Octave of ${octave.name}`, `In Octava ${octave.name}`)
: bi(`${ordinal(octave.dayNumber)} Day within the Octave of ${octave.name}`, `${ordinalLa(octave.dayNumber)} die infra Octavam ${octave.name}`);
? bi(`Octave of ${name.en}`, `In Octava ${name.la}`)
: bi(`${ordinal(octave.dayNumber)} Day within the Octave of ${name.en}`, `${ordinalLa(octave.dayNumber)} die infra Octavam ${name.la}`);
}
function octaveLabel(octave: ActiveOctave): Bi {
@@ -405,8 +441,8 @@ const CROSS_DAY_VESPERS_LABELS: Record<'today' | 'tomorrow', Bi> = {
* - `sanctoral`: every commemorated saint, plain name, no rank (multiple
* real ones can coexist — e.g. two colliding saints on the same date —
* this app's own "generous commemorations" design keeps every one of
* them, not just the first). No authored Latin saint names exist yet
* (see file header), so `la` falls back to the same English name.
* them, not just the first). `la` uses the saint's own `nameLa` when
* authored, else falls back to the same English name (see file header).
* - `temporal`: the day's own real Sunday/feria identity, if
* `decideOccurrence` demoted it to a commemoration (`ordinary-sunday`/
* `privileged-feria`/`privileged-feria-minor`) — matched by id against
@@ -438,19 +474,31 @@ function collectCommemorations(
const octaves: Bi[] = [];
for (const c of day.commemorations) {
if (c.kind === 'sanctoral') {
const name = biFallback(c.name);
sanctoral.push(
c.vespersNote
? bi(`${name.en} (${CROSS_DAY_VESPERS_LABELS[c.vespersNote].en})`, `${name.la} (${CROSS_DAY_VESPERS_LABELS[c.vespersNote].la})`)
: name,
);
const name = biName(c.name, c.nameLa);
if (c.vespersNote) {
sanctoral.push(bi(`${name.en} (${CROSS_DAY_VESPERS_LABELS[c.vespersNote].en})`, `${name.la} (${CROSS_DAY_VESPERS_LABELS[c.vespersNote].la})`));
} else if (c.transferredFrom) {
// Same "arrived via transfer, not natively due here" signal as a
// transferred-in *winner* gets — a reader shouldn't need to already
// know this saint's real date to realize a mere commemoration here
// is standing in for a transfer, not this date's own natural
// sanctoral standing.
sanctoral.push(bi(`${name.en} (${TRANSFERRED_LABEL.en})`, `${name.la} (${TRANSFERRED_LABEL.la})`));
} else {
sanctoral.push(name);
}
} else if (c.kind === 'temporal') {
if (c.id === temporalId) {
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) {
const octave = activeOctaves.find((a) => a.id === c.id);
octaves.push(octave ? octaveCommemorationLabel(octave) : biFallback(c.name));
octaves.push(octave ? octaveCommemorationLabel(octave) : biName(c.name, c.nameLa));
}
}
return { sanctoral, temporal, octaves };
@@ -500,16 +548,60 @@ const TEMPORAL_CATEGORY_RANK_LABELS: Record<TemporalCategory, Bi> = {
* silent about *why* a given identity governs tonight (a reader has to
* already know, e.g., that St. Zephyrinus falls on tomorrow's date to
* realize this evening is anticipating him). Pass the result of
* `calendar/vespers.ts`'s `resolveEveningDay`, not a plain `resolveDay`
* only that carries the `vespersFrom` tag this reads.
* `calendar/vespers.ts`'s `resolveEveningDay`, not a plain `resolveDay`,
* plus the originally-requested date — comparing the two directly (rather
* than checking `winner.vespersFrom`, which only ever gets set for a
* *sanctoral* tomorrow-winner, see `vespers.ts`'s `tagVespersFrom`) is
* what makes this correct even when tomorrow's winner is `temporal` (e.g.
* an ordinary Sunday winning outright over a merely-commemorated Simplex
* saint) — a case `winner.vespersFrom` can never detect since that field
* doesn't exist on the temporal variant of `DayWinner` at all.
*/
export function getVespersStatusLabel(day: LiturgicalDay): Bi {
if (day.winner.kind === 'sanctoral' && day.winner.vespersFrom === 'firstVespersOfTomorrow') {
export function getVespersStatusLabel(day: LiturgicalDay, requestedDate: string): Bi {
if (day.date !== requestedDate) {
return bi('First Vespers (of tomorrow)', 'Vesperæ de sequenti');
}
return bi('Second Vespers (of today)', 'Vesperæ de hodierno');
}
/** Appended after a transferred-in winner's name/rank, before any
* commemorations — same shape as a rank parenthetical. Distinct wording
* from `TRANSFERRED_AWAY_LABEL` below on purpose: this winner's own rank
* parenthetical sits right next to it ("St. X (Duplex, transferred)"), so
* plain "transferred" already reads as "arrived via transfer" in context —
* but on the origin date there's no rank alongside it to anchor that
* reading, so the away-note spells out the direction explicitly instead of
* relying on that same subtlety a first-time reader wouldn't have. */
const TRANSFERRED_LABEL = bi('transferred', 'translata');
/** See `TRANSFERRED_LABEL`'s doc comment for why this is worded differently. */
const TRANSFERRED_AWAY_LABEL = bi('transferred away', 'translata alio');
/** "St. X (transferred away)" note for a date whose own native sanctoral
* candidate (`day.transferredAway`) couldn't be kept here — appended after
* everything else `getDayLabel` already found for this date's real
* winner/commemorations, since the departure is a footnote about a
* *different* saint, not part of this date's own identity. Deliberately
* names no landing date (see `transferredAway`'s own doc comment) — the
* point is just to tell a reader "not an omission, this office moved
* elsewhere," not to claim a specific destination that a later, unmodeled
* transfer chain could make wrong. */
function transferredAwayNote(day: LiturgicalDay): Bi | undefined {
if (!day.transferredAway) {
return undefined;
}
const name = biName(day.transferredAway.candidate.name, day.transferredAway.candidate.nameLa);
return bi(`${name.en} (${TRANSFERRED_AWAY_LABEL.en})`, `${name.la} (${TRANSFERRED_AWAY_LABEL.la})`);
}
function withTransferNote(day: LiturgicalDay, label: Partial<Record<string, string>>): Partial<Record<string, string>> {
const note = transferredAwayNote(day);
if (!note) {
return label;
}
return { en: `${label.en}${note.en}`, la: `${label.la}${note.la}` };
}
/**
* The full "day being celebrated" label: the day's own primary identity
* (a sanctoral winner, a named temporal feast, a season's own anchor day,
@@ -536,9 +628,11 @@ export function getDayLabel(day: LiturgicalDay): Partial<Record<string, string>>
showOctaves: day.temporalCategory === 'ordinary-feria',
});
const rank = formatRank(day.winner.rank);
const winnerName = biFallback(day.winner.name);
const winner = bi(`${winnerName.en} (${rank.en})`, `${winnerName.la} (${rank.la})`);
return joinBi([winner, ...temporal, ...sanctoral, ...octaves]);
const winnerName = biName(day.winner.name, day.winner.nameLa);
const winner = day.winner.transferredFrom
? bi(`${winnerName.en} (${rank.en}, ${TRANSFERRED_LABEL.en})`, `${winnerName.la} (${rank.la}, ${TRANSFERRED_LABEL.la})`)
: bi(`${winnerName.en} (${rank.en})`, `${winnerName.la} (${rank.la})`);
return withTransferNote(day, joinBi([winner, ...temporal, ...sanctoral, ...octaves]));
}
// A named temporal feast (Christmas, Pentecost, Marian Saturday, ...)
@@ -548,12 +642,12 @@ export function getDayLabel(day: LiturgicalDay): Partial<Record<string, string>>
// temporal-feasts.ts) and fall through further down.
const namedFeast = getTemporalFeastRecord(day.winner.id);
if (namedFeast) {
const name = biFallback(namedFeast.name);
const name = biName(namedFeast.name, namedFeast.nameLa);
const primary = namedFeast.rank
? bi(`${name.en} (${formatRank(namedFeast.rank).en})`, `${name.la} (${formatRank(namedFeast.rank).la})`)
: name;
const { sanctoral } = collectCommemorations(day, { showOctaves: false });
return joinBi([primary, ...sanctoral]);
return withTransferNote(day, joinBi([primary, ...sanctoral]));
}
// A season's own named anchor day (Trinity Sunday, Easter, Ash
@@ -561,12 +655,12 @@ export function getDayLabel(day: LiturgicalDay): Partial<Record<string, string>>
// anchorDayName's own doc comment — checked before the octave case
// below since Trinity Sunday, e.g., also happens to be day 8 of
// Pentecost's octave, and the anchor name is what actually governs.
// Existing convention: a commemorated saint *leads* here, before the
// anchor name — not "winner first" like every branch above.
// Winner first, same as every other branch here — the anchor day is
// what's actually being celebrated even when a lesser saint rides along.
const anchorName = anchorDayName(day);
if (anchorName) {
const { sanctoral } = collectCommemorations(day, { showOctaves: false });
return joinBi([...sanctoral, anchorName]);
return withTransferNote(day, joinBi([anchorName, ...sanctoral]));
}
// An active octave (St. Lawrence's, ...) is this day's real primary
@@ -602,12 +696,12 @@ export function getDayLabel(day: LiturgicalDay): Partial<Record<string, string>>
if (activeOctave) {
const { sanctoral, octaves } = collectCommemorations(day, { showOctaves: true, excludeOctaveId: activeOctave.id });
const primary = octaveLabel(activeOctave);
return joinBi([primary, ...octaves, ...sanctoral]);
return withTransferNote(day, joinBi([primary, ...octaves, ...sanctoral]));
}
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 { sanctoral } = collectCommemorations(day, { showOctaves: false });
return joinBi([...sanctoral, temporal]);
return withTransferNote(day, joinBi([temporal, ...sanctoral]));
}
+18 -1
View File
@@ -15,6 +15,9 @@ import sanctoralCalendarData from '../data/calendar/sanctoral-calendar.yml';
export interface SaintRecord {
id: string;
name: string;
/** Latin form of `name`, when authored see calendar/types.ts's
* `SanctoralIdentity.nameLa` doc comment for the display fallback. */
nameLa?: string;
rank: FeastClass;
common: string;
propers: string | null;
@@ -75,6 +78,19 @@ export interface SaintRecord {
* independently per saint, not assumed identical).
*/
benedictusCommon?: string;
/**
* The `{N}` substitution value(s) for the Matins invitatory antiphon's
* own Common template (`hours/matins.ts`'s `resolveMatinsInvitatoryText`,
* via `hours/resolve-common.ts`'s `substituteName` the same helper
* `getDayCollect` already uses for `collectCommon`/`collectName`, just a
* separate field here since the invitatory's own `{N}` position needs
* its own grammatical case, not necessarily the same one `collectName`
* was declined for). Currently only needed for Common-of-a-Holy-Woman
* (`Commune/C7.txt`'s own `[Invit]`, "Laudémus Deum nostrum * In
* confessióne beátæ N.." — genitive singular, e.g. "Marthæ") every
* other Common category in use has name-free invitatory text. Absent
* for saints whose Common invitatory antiphon needs no substitution. */
invitatoryName?: { la: string[]; en: string[] };
/** See calendar/types.ts's OctaveConfig doc comment. Absent for the vast
* majority of saints only ones with a real octave declare it. */
octave?: OctaveConfig;
@@ -125,6 +141,7 @@ for (const mod of Object.values(saintModules)) {
export interface SanctoralCandidate {
id: string;
name: string;
nameLa?: string;
rank: FeastClass;
}
@@ -144,6 +161,6 @@ export function getSanctoralCandidatesFor(isoDate: string): SanctoralCandidate[]
if (!saint) {
throw new Error(`sanctoral-calendar.yml references unknown saint id '${id}'`);
}
return { id: saint.id, name: saint.name, rank: saint.rank };
return { id: saint.id, name: saint.name, nameLa: saint.nameLa, rank: saint.rank };
});
}
+179 -32
View File
@@ -1,9 +1,9 @@
import type { Commemoration, DayWinner, LiturgicalDay, SanctoralIdentity, TemporalCategory } from './types';
import type { Commemoration, DayWinner, FeastClass, LiturgicalDay, SanctoralIdentity, TemporalCategory } from './types';
import { weekdayOf } from './weekday';
import { resolveSeason, resolveTemporalCategory } from './temporal';
import { resolveSeason, resolveTemporalCategory, easterOffsetOf } from './temporal';
import { resolveTemporalId } from './temporal-id';
import { getSanctoralCandidatesFor } from './feasts';
import { decideOccurrence, compareFeastClass, type OccurrenceResult } from './commemorations';
import { decideOccurrence, compareFeastClass, isAtLeast, type OccurrenceResult } from './commemorations';
import { resolveCollision } from './collision';
import { addDays, toIsoDate } from './date-math';
import { easterSunday } from './easter';
@@ -13,10 +13,13 @@ import { applyEmberDay } from './ember-days';
/**
* A day's occurrence considered on its own no awareness of what an
* adjacent day might be trying to transfer in. `resolveDay` calls this for
* the date itself *and* for yesterday/tomorrow (to check for an inbound
* transfer) without ever recursing into their own adjacent-day checks
* that's what keeps this from being mutually recursive.
* adjacent day might be trying to transfer in. `resolveDay` calls this
* for the date itself and for yesterday/tomorrow (to check for an
* inbound transfer) without ever recursing into their own adjacent-day
* checks that's what keeps this from being mutually recursive. The one
* exception, `easterOctaveBacklogFor`, deliberately calls this for up to
* 15 fixed days (Palm Sunday through Low Sunday) regardless of `isoDate`
* see its own doc comment.
*/
function resolveNativeOccurrence(isoDate: string): { temporalCategory: TemporalCategory; result: OccurrenceResult } {
const weekday = weekdayOf(isoDate);
@@ -37,7 +40,12 @@ function resolveNativeOccurrence(isoDate: string): { temporalCategory: TemporalC
const collision = resolveCollision(candidate, topCandidate);
clashCommemorations.push(...collision.commemorations);
if (collision.winner.kind === 'sanctoral') {
topCandidate = { id: collision.winner.id, name: collision.winner.name, rank: collision.winner.rank };
topCandidate = {
id: collision.winner.id,
name: collision.winner.name,
nameLa: collision.winner.nameLa,
rank: collision.winner.rank,
};
}
}
@@ -51,17 +59,34 @@ function applyIncomingTransfer(
temporalCategory: TemporalCategory,
winner: DayWinner,
commemorations: Commemoration[],
fromDate: string,
): DayWinner {
if (temporalCategory === 'privileged-feria-major') {
// Deferred: a transfer landing on one of these (the concrete case is
// Holy Week, right after Palm Sunday) needs its own Easter-date-keyed
// lookup table, the same way the reference engine handles it — not
// modeled yet. The transfer is simply not delivered here rather than
// guessed at. The lesser privileged-feria tier doesn't need this —
// ordinary collision/transfer logic is fine there, same as any
// ordinary-sunday landing.
// Never a landing spot for any rank, and never even a bare
// commemoration (commemorations.ts's own case already guarantees
// this — a `winner.kind === 'sanctoral'` day never actually happens
// in this category, so `decideOccurrence` alone would already
// produce this same no-op; kept explicit as a documented short-
// circuit). A candidate impeded here — Holy Week, the Easter Octave,
// Low Sunday — doesn't chain forward day-by-day the way an ordinary
// transfer does; see `applyEasterOctaveBacklog` in `resolveDay` for
// its own dedicated mechanism instead. The lesser privileged-feria
// tier doesn't need any of this — ordinary collision/transfer logic
// is fine there, same as any ordinary-sunday landing.
return winner;
}
// Tags the returned winner with where it transferred from, but only when
// the candidate actually ended up winning here — a collision loss (the
// candidate merely commemorated, incumbent keeps the day) isn't a landing.
const tagIfLanded = (result: DayWinner): DayWinner =>
result.kind === 'sanctoral' && result.id === candidate.id ? { ...result, transferredFrom: fromDate } : result;
// Same tagging, but for the candidate's own *commemoration* — it still
// needs marking as arrived-via-transfer even when it lost the landing
// day outright (a collision loss, or falling below the landing day's own
// threshold in decideOccurrence) rather than merely being naturally due
// for commemoration here.
const tagCommemorations = (result: Commemoration[]): Commemoration[] =>
result.map((c) => (c.kind === 'sanctoral' && c.id === candidate.id ? { ...c, transferredFrom: fromDate } : c));
if (winner.kind === 'temporal') {
// Found via real data: a transferred-in Simplex saint was blindly
// winning outright even when it landed on a privileged Sunday (Trinity
@@ -69,15 +94,84 @@ function applyIncomingTransfer(
// the *same* precedence rules a native occurrence would have used,
// not just take over because nothing else was assigned here.
const decided = decideOccurrence(temporalCategory, winner.id, candidate);
commemorations.push(...decided.commemorations);
// If the candidate fails here too (decided.transfer set), that's a
// transfer chain — not modeled, same "not delivered rather than
// guessed at" stance as the privileged-feria-major case above.
return decided.winner;
commemorations.push(...tagCommemorations(decided.commemorations));
// If the candidate fails here too (decided.transfer set), it's simply
// not delivered — every category reaching this branch only ever
// needs a single hop (see `applyEasterOctaveBacklog` for the one
// multi-day case, handled separately, not through here).
return tagIfLanded(decided.winner);
}
const collision = resolveCollision(candidate, winner);
commemorations.push(...collision.commemorations);
return collision.winner;
commemorations.push(...tagCommemorations(collision.commemorations));
return tagIfLanded(collision.winner);
}
// The minimum rank worth carrying out of the Palm-Sunday-through-Low-
// Sunday span at all (see `applyEasterOctaveBacklog` below) — anything
// weaker than this just lapses for the year, same as it already gets no
// trace *within* the span itself (commemorations.ts's `privileged-feria-
// major`/`privileged-sunday` cases). Per direct instruction (2026-09-01):
// this is the same threshold the sibling in-place-commemoration rule
// used before it was found wrong — that number wasn't the mistake, only
// which question it was answering.
const EASTER_OCTAVE_BACKLOG_FLOOR: FeastClass = 'duplex-majus';
/**
* Every candidate impeded anywhere from Palm Sunday through Low Sunday
* inclusive (`easterOffsetOf` -7 through +7) that clears
* `EASTER_OCTAVE_BACKLOG_FLOOR`, in native-date order the backlog
* `applyEasterOctaveBacklog` draws from. Judged only by each day's own
* `resolveNativeOccurrence`, never a fully `resolveDay`-resolved one
* (same convention `applyIncomingTransfer` already uses).
*/
function easterOctaveBacklogFor(year: number): { candidate: SanctoralIdentity; fromDate: string }[] {
const easterIso = toIsoDate(easterSunday(year));
const backlog: { candidate: SanctoralIdentity; fromDate: string }[] = [];
for (let offset = -7; offset <= 7; offset++) {
const date = addDays(easterIso, offset);
const candidate = resolveNativeOccurrence(date).result.transfer?.candidate;
if (candidate && isAtLeast(candidate.rank, EASTER_OCTAVE_BACKLOG_FLOOR)) {
backlog.push({ candidate, fromDate: date });
}
}
return backlog;
}
// Generous headroom over any realistic backlog length (the whole span is
// 15 days; vu's own calendar has never produced more than a handful of
// duplex-majus+ candidates in it) — exists purely so a date far removed
// from any Easter doesn't pay for an `easterOctaveBacklogFor` computation
// it could never need.
const MAX_EASTER_OCTAVE_BACKLOG_DAYS = 60;
/**
* The Nth day after Low Sunday celebrates the Nth candidate in that
* year's backlog (native-date order) winning outright, no rank fight
* against whatever would have natively been there, which is demoted to a
* commemoration instead one candidate per day until the backlog is
* exhausted. Per direct instruction (2026-09-01): not a generic multi-day
* cascade/queue (this app already has real precedent for stacking
* commemorations, so a spread-out cascade wasn't the right shape), and
* not a same-day batch either (only one candidate surfaces per day, same
* as any other transfer landing in this app) a fixed, deterministic
* position for each backlog candidate rather than a "first day not
* already spoken for" search.
*/
function applyEasterOctaveBacklog(isoDate: string, winner: DayWinner, commemorations: Commemoration[]): DayWinner {
const offset = easterOffsetOf(isoDate);
if (offset <= 7 || offset > 7 + MAX_EASTER_OCTAVE_BACKLOG_DAYS) {
return winner;
}
const n = offset - 7;
const backlog = easterOctaveBacklogFor(Number(isoDate.slice(0, 4)));
const entry = backlog[n - 1];
if (!entry) {
return winner;
}
if (winner.kind === 'sanctoral') {
commemorations.push({ kind: 'sanctoral', id: winner.id, name: winner.name, nameLa: winner.nameLa, rank: winner.rank });
}
return { kind: 'sanctoral', ...entry.candidate, transferredFrom: entry.fromDate };
}
/**
@@ -94,14 +188,34 @@ export function resolveDay(isoDate: string): LiturgicalDay {
let winner = result.winner;
const commemorations = [...result.commemorations];
const yesterday = resolveNativeOccurrence(addDays(isoDate, -1));
if (yesterday.result.transfer?.direction === 'forward') {
winner = applyIncomingTransfer(yesterday.result.transfer.candidate, temporalCategory, winner, commemorations);
const yesterdayIso = addDays(isoDate, -1);
const yesterday = resolveNativeOccurrence(yesterdayIso);
// A candidate impeded anywhere from Palm Sunday through Low Sunday
// (`easterOffsetOf` -7 through +7) is exclusively
// `applyEasterOctaveBacklog`'s to place — skipped here so it isn't
// *also* picked up by this ordinary single-hop check (real bug found
// 2026-09-01: Low Sunday's own transfer was leaking into the very next
// day's ordinary yesterday-check, landing/commemorating a candidate
// below the backlog's own rank floor). Scoped to that specific span,
// not `privileged-sunday` generally — an ordinary privileged Sunday
// elsewhere (Advent, SeptuagesimaPassiontide) still uses this normal
// single-hop path same as before.
const yesterdayOffset = easterOffsetOf(yesterdayIso);
const yesterdayInEasterSpan = yesterdayOffset >= -7 && yesterdayOffset <= 7;
if (yesterday.result.transfer?.direction === 'forward' && !yesterdayInEasterSpan) {
winner = applyIncomingTransfer(yesterday.result.transfer.candidate, temporalCategory, winner, commemorations, yesterdayIso);
}
const tomorrow = resolveNativeOccurrence(addDays(isoDate, 1));
const tomorrowIso = addDays(isoDate, 1);
const tomorrow = resolveNativeOccurrence(tomorrowIso);
if (tomorrow.result.transfer?.direction === 'backward') {
winner = applyIncomingTransfer(tomorrow.result.transfer.candidate, temporalCategory, winner, commemorations);
winner = applyIncomingTransfer(tomorrow.result.transfer.candidate, temporalCategory, winner, commemorations, tomorrowIso);
}
// The one case that single yesterday-check can't reach: anything
// impeded within Holy Week/Easter Day/the Octave/Low Sunday itself
// (all `privileged-feria-major`/`privileged-sunday`, which never
// accept a landing at all) needs its own dedicated mechanism — see
// `applyEasterOctaveBacklog`'s own doc comment.
winner = applyEasterOctaveBacklog(isoDate, winner, commemorations);
winner = applyOctaves(isoDate, winner, commemorations);
winner = applyEmberDay(isoDate, winner, commemorations);
@@ -109,8 +223,15 @@ export function resolveDay(isoDate: string): LiturgicalDay {
winner = applyChristmasOctaveSunday(isoDate, weekday, winner, commemorations);
winner = applyMovableFeasts(isoDate, winner, commemorations);
applyEpiphany6Commemoration(isoDate, commemorations);
applyAdventFourVigilCommemoration(isoDate, weekday, commemorations);
return { date: isoDate, weekday, season, temporalCategory, winner, commemorations };
// This date's own native candidate (if any) couldn't be kept here and
// moved on instead — see calendar/types.ts's `transferredAway` doc
// comment for why this is tracked separately from `Commemoration`, and
// why no landing date is recorded.
const transferredAway = result.transfer ? { candidate: result.transfer.candidate } : undefined;
return { date: isoDate, weekday, season, temporalCategory, winner, commemorations, transferredAway };
}
/**
@@ -141,6 +262,32 @@ function applyEpiphany6Commemoration(isoDate: string, commemorations: Commemorat
commemorations.push({ kind: 'temporal', id: 'post-epiphany-6' });
}
/**
* When Advent 4's own Sunday lands on Dec 24 (its latest possible date),
* `resolveTemporalId` already gives that day the Vigil of Christmas's own
* identity (`vigil-of-christmas` collect, Lauds antiphons, Matins Gospel
* nocturn), matching the reference engine's own rule that the Vigil's rank
* governs. Per direct instruction (2026-08), unlike the reference engine's
* own `no commemoratio` for this exact day, Advent 4 itself is still
* commemorated here the Sundays of Advent have real standing of their
* own, distinct from an ordinary saint quietly ceding to a Vigil. Purely
* additive, same shape as applyEpiphany6Commemoration just above: no new
* `Commemoration` variant needed, `getDayCollects` already resolves any
* `{ kind: 'temporal' }` commemoration's own `${id}-collect` for free.
* `advent-4` is hardcoded rather than recomputed because Dec 24 always
* falls at or after Advent's own 4th Sunday by construction (it's the
* latest date Advent's 4th Sunday can ever land on).
*/
function applyAdventFourVigilCommemoration(
isoDate: string,
weekday: ReturnType<typeof weekdayOf>,
commemorations: Commemoration[],
): void {
if (isoDate.slice(5) === '12-24' && weekday === 'sunday') {
commemorations.push({ kind: 'temporal', id: 'advent-4' });
}
}
/**
* Per direct instruction, scoped to exactly Dec 26-30 every year
* contains exactly one real Sunday somewhere in that 5-day window.
@@ -210,7 +357,7 @@ function applyChristmasOctaveSunday(
if (idx !== -1) {
commemorations.splice(idx, 1);
}
return { kind: 'sanctoral', id: displaced.id, name: displaced.name, rank: displaced.rank };
return { kind: 'sanctoral', id: displaced.id, name: displaced.name, nameLa: displaced.nameLa, rank: displaced.rank };
}
}
@@ -263,7 +410,7 @@ function applyMarianSaturday(
if (winner.rank !== 'simplex') {
return winner;
}
commemorations.push({ kind: 'sanctoral', id: winner.id, name: winner.name, rank: winner.rank });
commemorations.push({ kind: 'sanctoral', id: winner.id, name: winner.name, nameLa: winner.nameLa, rank: winner.rank });
return MARIAN_SATURDAY;
}
@@ -316,7 +463,7 @@ function applyOctaves(isoDate: string, winner: DayWinner, commemorations: Commem
if (cmp < 0 || tiedAgainstClosingDay) {
const isOneOfTheseOctaves = octaves.some((o) => o.id === winner.id);
if (!isOneOfTheseOctaves) {
commemorations.push({ kind: 'sanctoral', id: winner.id, name: winner.name, rank: winner.rank });
commemorations.push({ kind: 'sanctoral', id: winner.id, name: winner.name, nameLa: winner.nameLa, rank: winner.rank });
resolvedWinner = { kind: 'temporal', id: resolveTemporalId(isoDate) };
}
}
@@ -329,7 +476,7 @@ function applyOctaves(isoDate: string, winner: DayWinner, commemorations: Commem
// again alongside itself would be redundant.
const isOwnStartDay = octave.dayNumber === 1 && resolvedWinner.kind === 'temporal';
if (!isSelf && !isOwnStartDay) {
commemorations.push({ kind: 'octave', id: octave.id, name: octave.name });
commemorations.push({ kind: 'octave', id: octave.id, name: octave.name, nameLa: octave.nameLa });
}
}
+2 -2
View File
@@ -83,7 +83,7 @@ export function applyMovableFeasts(isoDate: string, winner: DayWinner, commemora
if (resolvedWinner.kind === 'temporal') {
commemorations.push({ kind: 'temporal', id: resolvedWinner.id });
} else {
commemorations.push({ kind: 'sanctoral', id: resolvedWinner.id, name: resolvedWinner.name, rank: resolvedWinner.rank });
commemorations.push({ kind: 'sanctoral', id: resolvedWinner.id, name: resolvedWinner.name, nameLa: resolvedWinner.nameLa, rank: resolvedWinner.rank });
}
resolvedWinner = FEAST_WINNER;
continue;
@@ -95,7 +95,7 @@ export function applyMovableFeasts(isoDate: string, winner: DayWinner, commemora
} else if (compareFeastClass(resolvedWinner.rank, rank) >= 0) {
commemorations.push({ kind: 'temporal', id: feast.id });
} else {
commemorations.push({ kind: 'sanctoral', id: resolvedWinner.id, name: resolvedWinner.name, rank: resolvedWinner.rank });
commemorations.push({ kind: 'sanctoral', id: resolvedWinner.id, name: resolvedWinner.name, nameLa: resolvedWinner.nameLa, rank: resolvedWinner.rank });
resolvedWinner = FEAST_WINNER;
}
}
+7 -3
View File
@@ -17,6 +17,9 @@ import { compareFeastClass, minimumOutrightWinningRank } from './commemorations'
export interface ActiveOctave {
id: string;
name: string;
/** Latin form of `name`, when authored see calendar/types.ts's
* `SanctoralIdentity.nameLa` doc comment for the display fallback. */
nameLa?: string;
/** This octave's own effective rank *today* the ordinary `wins`
* threshold on every day except its own closing day, where it's
* `closingDayRank` instead (see calendar/types.ts's OctaveConfig).
@@ -49,6 +52,7 @@ function considerCandidate(
startDate: string,
id: string,
name: string,
nameLa: string | undefined,
octave: OctaveConfig | undefined,
): void {
if (!octave?.enabled || seen.has(id)) {
@@ -63,7 +67,7 @@ function considerCandidate(
const dayNumber = offset + 1;
const isClosingDay = dayNumber === days;
const wins = isClosingDay ? (octave.closingDayRank ?? DEFAULT_CLOSING_DAY_RANK) : (octave.wins ?? DEFAULT_WINS);
active.push({ id, name, wins, dayNumber, isClosingDay });
active.push({ id, name, nameLa, wins, dayNumber, isClosingDay });
}
/** Every octave (sanctoral or temporal) whose window covers `isoDate`,
@@ -80,14 +84,14 @@ export function activeOctavesFor(isoDate: string): ActiveOctave[] {
for (const candidate of getSanctoralCandidatesFor(candidateDate)) {
const record = getSaintRecord(candidate.id);
if (record) {
considerCandidate(active, seen, isoDate, candidateDate, record.id, record.name, record.octave);
considerCandidate(active, seen, isoDate, candidateDate, record.id, record.name, record.nameLa, record.octave);
}
}
for (const feastId of temporalFeastIdsStartingOn(candidateDate)) {
const record = getTemporalFeastRecord(feastId);
if (record) {
considerCandidate(active, seen, isoDate, candidateDate, record.id, record.name, record.octave);
considerCandidate(active, seen, isoDate, candidateDate, record.id, record.name, record.nameLa, record.octave);
}
}
}
+4
View File
@@ -30,6 +30,9 @@ export type MovableAnchor =
export interface TemporalFeastRecord {
id: string;
name: string;
/** Latin form of `name`, when authored see calendar/types.ts's
* `SanctoralIdentity.nameLa` doc comment for the display fallback. */
nameLa?: string;
octave?: OctaveConfig;
rank?: FeastClass;
anchor?: MovableAnchor;
@@ -87,6 +90,7 @@ const FIXED_DATE_STARTS: [string, string][] = [['12-25', 'christmas-day']];
/** Easter-offset starts (offset -> temporal feast id). */
const EASTER_OFFSET_STARTS: [number, string][] = [
[17, 'patronage-of-st-joseph'],
[39, 'ascension'],
[49, 'pentecost-sunday'],
[60, 'corpus-christi'],
+18
View File
@@ -78,6 +78,24 @@ export function resolveTemporalId(isoDate: string): string {
const season = resolveSeason(isoDate);
const year = Number(isoDate.slice(0, 4));
// The Vigil of Christmas (Dec 24) has its own real proper content
// (collect, Lauds antiphons, Matins Gospel+homily nocturn) distinct from
// Advent 4's own — see data/propers/temporal/vigil-of-christmas-collect.yml
// and data/propers/nocturn-readings/vigil-of-christmas.yml, both
// transcribed directly from the reference engine's Sancti/12-24.txt.
// Applies even when Dec 24 itself is Advent 4's own Sunday — per direct
// instruction (2026-08), the Vigil's own identity (rank/collect/Lauds
// antiphons) governs that day too, with Advent 4 folded in as a
// commemoration instead (calendar/index.ts's
// applyAdventFourVigilCommemoration) rather than the reference engine's
// own `no commemoratio` rule. Matins' three-nocturn Sunday psalmody is
// unaffected either way — it's driven by `day.weekday`, not by this id
// (hours/matins.ts's own `threeNocturns` check), so Advent 4's "Psalmi
// Dominica" standing survives this id swap for free.
if (isoDate.slice(5) === '12-24') {
return 'vigil-of-christmas';
}
if (season === 'advent') {
const start = adventStart(year);
const n = Math.round(daysBetween(start, sundayOnOrBefore(isoDate)) / 7) + 1;
+22 -1
View File
@@ -202,12 +202,33 @@ export function adventEmberDayOffset(isoDate: string): number | undefined {
* the Christmas vigil (Dec 24 can land on a Sunday) and a Sunday's own
* privileged/ordinary status should win in that case regardless, so
* Sundays are resolved straight from `bySeason` without consulting the
* overrides at all.
* overrides at all. The deliberate exceptions are Easter Sunday itself
* (offset 0) and Low Sunday (offset 7, the Octave's own closing day):
* `eastertide`'s own `bySeason` default is `ordinary-sunday` (live-
* verified for the *ordinary* Sundays from the 2nd Sunday after Easter
* on see that entry's own comment), but both bookend Sundays of the
* Easter Octave need the stronger `privileged-sunday` tier instead.
* Easter Day: live-verified 2026-09-01, the Annunciation (Duplex I.
* classis) transfers off it in years they coincide, the same "transfers,
* no exception" behavior as every other privileged-sunday case. Low
* Sunday: not independently collision-verified (no Monastic-track saint
* `SanctiM/MM-DD.txt` ever falls there across the real range of
* Easter dates, so no live test case exists), but its own formal rank in
* the reference source ("Dominica in Albis in Octava Paschæ ~ Duplex I.
* classis") matches Easter Day's own exactly, and no `Transfer:` note
* ever shows there even for a same-named non-Monastic-track saint
* (St. Isidore of Seville, 2027; St. Vincent Ferrer, 2043) that a real
* Monastic-track day would display treated the same as Easter Day on
* that basis. Both checked directly here rather than via `offsets`,
* since offsets are never consulted for a Sunday.
*/
export function resolveTemporalCategory(isoDate: string, season: Season, weekday: Weekday): TemporalCategory {
const bySeasonEntry = temporalCategories.bySeason[season];
const base = bySeasonEntry ? (weekday === 'sunday' ? bySeasonEntry.sunday : bySeasonEntry.feria) : 'ordinary-feria';
if (weekday === 'sunday') {
if (season === 'eastertide' && [0, 7].includes(easterOffsetOf(isoDate))) {
return 'privileged-sunday';
}
return base;
}
+46 -7
View File
@@ -76,12 +76,16 @@ export type FeastClass =
// 1617 engine, which the original single-tier model wrongly forbade) —
// see calendar/commemorations.ts's rules for `privileged-feria` (behaves
// like `ordinary-sunday`: Duplex+ wins outright) vs `privileged-feria-major`
// (behaves like `privileged-sunday`: never displaced at all, confirmed by
// checking St. Mark, Duplex II. classis, merely commemorated rather than
// winning within the Easter Octave). See data/calendar/temporal-
// categories.yml for exactly which days fall in which tier — some of that
// split (e.g. whether Pentecost's own Ember days share Lent's lesser tier
// or Easter's major one) is still a reconstructed guess, not verified.
// (behaves like `privileged-sunday`: never displaced, and — corrected
// 2026-09-01 — never gets even a bare commemoration at any rank either;
// an earlier claim that a duplex-majus+ candidate is "merely commemorated"
// there, citing St. Mark within the Easter Octave, turned out to be a
// misreading — direct instruction confirmed nothing is ever commemorated
// anywhere from Palm Sunday through Low Sunday, and live-checking Ash
// Wednesday found the same). See data/calendar/temporal-categories.yml
// for exactly which days fall in which tier — some of that split (e.g.
// whether Pentecost's own Ember days share Lent's lesser tier or
// Easter's major one) is still a reconstructed guess, not verified.
// `privileged-feria-minor` is a third, weaker tier, added during the
// December sanctoral pull after finding Advent's own ordinary (non-Ember)
// ferias have real standing of their own in the live engine — a Simplex
@@ -103,6 +107,11 @@ export type TemporalCategory =
export interface SanctoralIdentity {
id: string;
name: string;
/** Latin form of `name`, when authored (see `data/calendar/saints/*.yml`'s
* own `nameLa` field). Absent for the majority of saints still display
* code falls back to `name` itself when this is missing, same "surface
* real text rather than block on missing" convention used elsewhere. */
nameLa?: string;
rank: FeastClass;
}
@@ -156,6 +165,12 @@ export type DayWinner =
// First Vespers is being anticipated this evening (i.e. this feast
// belongs to *tomorrow*, but is winning tonight's Vespers/Compline).
vespersFrom?: 'firstVespersOfTomorrow';
// Set by calendar/index.ts's resolveDay/applyIncomingTransfer when
// this winner is only here because its own native date (an adjacent
// ISO date) couldn't hold it (calendar/commemorations.ts's
// `decideOccurrence` `transfer` signal) — the ISO date it transferred
// *from*. Absent for a feast winning on its own native date.
transferredFrom?: string;
} & SanctoralIdentity);
/**
@@ -179,8 +194,16 @@ export type Commemoration =
// commemoration (a real collision, an octave day, etc.), which
// carries no such qualifier.
vespersNote?: 'today' | 'tomorrow';
// Set by calendar/index.ts's applyIncomingTransfer, same meaning as
// `DayWinner`'s own `transferredFrom` — this saint is only
// commemorated here (rather than absent) because a transfer landed
// on this date and lost a collision, or fell below the landing
// day's own outright-winning threshold, not because it's natively
// due for commemoration here. Absent for an ordinary same-day
// commemoration.
transferredFrom?: string;
} & SanctoralIdentity)
| { kind: 'octave'; id: string; name: string };
| { kind: 'octave'; id: string; name: string; nameLa?: string };
export interface LiturgicalDay {
/** ISO date, e.g. "2026-08-09" */
@@ -194,4 +217,20 @@ export interface LiturgicalDay {
winner: DayWinner;
/** Everything else commemorated alongside the winner — see the doc comment on Commemoration. */
commemorations: Commemoration[];
/** Set when this date's own native sanctoral candidate couldn't be kept
* here at all (calendar/commemorations.ts's `decideOccurrence` `transfer`
* signal) and moved to a later/earlier date instead. No landing date is
* recorded here this exists purely so the display can say "not an
* omission, this office moved elsewhere" without needing the caller to
* separately go find where; the actual landing date is discoverable
* from the landing day's own winner/commemoration, tagged with
* `transferredFrom`. An ordinary transfer (calendar/index.ts's
* `applyIncomingTransfer`, called from `resolveDay`) only ever needs
* the immediately adjacent day; anything impeded within Holy Week/the
* Easter Octave/Low Sunday goes through the separate, dedicated
* `applyEasterOctaveBacklog` mechanism instead, which can place a
* candidate many days later. Distinct from and not implied by
* `Commemoration`, since a transferred-away
* candidate gets no commemoration on its own native date at all. */
transferredAway?: { candidate: SanctoralIdentity };
}
+47 -12
View File
@@ -12,7 +12,7 @@ import type { Commemoration, DayWinner, LiturgicalDay } from './types';
import { resolveDay } from './index';
import { addDays } from './date-math';
import { easterOffsetOf } from './temporal';
import { isAtLeast } from './commemorations';
import { compareFeastClass, isAtLeast } from './commemorations';
import { getSaintRecord } from './feasts';
import { getTemporalFeastRecord } from './temporal-feasts';
@@ -53,18 +53,27 @@ function hasFirstVespers(day: LiturgicalDay): boolean {
return day.winner.kind === 'sanctoral';
}
/** Does this day keep its own Second Vespers regardless of what tomorrow is? */
/** Does this day keep its own Second Vespers regardless of what tomorrow is,
* independent of any comparison against tomorrow's rank? Covers only the
* *absolute* cases Sunday, a privileged feria, or a major fixed feast of
* the Lord. The ordinary sanctoral-vs-sanctoral case is decided separately,
* by direct rank comparison (see `resolveEveningDay`) rather than a fixed
* floor here: this function used to also require the winner be at least
* `duplex-2-classis`, but that's the wrong rule the real "De Concurrentia
* Officii" rubric (divinum-officium-reference's rubrics.txt, Titulus XI)
* compares today's rank against tomorrow's directly, with ties going to
* tomorrow, not a fixed threshold on today alone. That floor produced a
* wrong result for 2026-08-31 -> 09-01: St. Raymond Nonnatus (Semiduplex,
* below the floor) lost his own Second Vespers to St. Giles (Simplex) even
* though Raymond outranks Giles. */
function keepsOwnSecondVespers(day: LiturgicalDay): boolean {
if (
return (
day.weekday === 'sunday' ||
day.temporalCategory === 'privileged-feria-minor' ||
day.temporalCategory === 'privileged-feria' ||
day.temporalCategory === 'privileged-feria-major' ||
isMajorFixedFeastOfTheLord(day.date)
) {
return true;
}
return day.winner.kind === 'sanctoral' && isAtLeast(day.winner.rank, 'duplex-2-classis');
);
}
/**
@@ -85,7 +94,7 @@ function commemorationOfDisplacedPredecessor(loser: DayWinner): Commemoration |
if (loser.kind !== 'sanctoral' || loser.rank === 'simplex') {
return undefined;
}
return { kind: 'sanctoral', id: loser.id, name: loser.name, rank: loser.rank, vespersNote: 'today' };
return { kind: 'sanctoral', id: loser.id, name: loser.name, nameLa: loser.nameLa, rank: loser.rank, vespersNote: 'today' };
}
/**
@@ -120,7 +129,7 @@ function commemorationOfDisplacedSuccessor(winner: DayWinner, loser: DayWinner):
if (forbids) {
return undefined;
}
return { kind: 'sanctoral', id: loser.id, name: loser.name, rank: loser.rank, vespersNote: 'tomorrow' };
return { kind: 'sanctoral', id: loser.id, name: loser.name, nameLa: loser.nameLa, rank: loser.rank, vespersNote: 'tomorrow' };
}
function tagVespersFrom(tomorrow: LiturgicalDay): LiturgicalDay {
@@ -200,6 +209,23 @@ function anticipated(today: LiturgicalDay, tomorrow: LiturgicalDay): LiturgicalD
* would likewise displace an actual Sunday if one ever landed on Dec 24 or
* an Ember Saturday. This is why it's a short, explicit list rather than a
* threshold: these particular days are understood to be *absolute*.
*
* When neither side is covered by an absolute case and both today and
* tomorrow have a real sanctoral winner, the rubric's actual rule applies:
* direct rank comparison, with today keeping its own Second Vespers only if
* it *strictly* outranks tomorrow an exact tie goes to tomorrow ("a
* Capitulo fit de sequenti cum commemoratione praecedentis"). Note this
* checks tomorrow's actual *winner*, not merely whether tomorrow's calendar
* weekday is Sunday a plain Duplex can itself outright win an ordinary
* Sunday's daytime office (see commemorations.ts's header comment, e.g. St.
* Anne beating an ordinary Sunday on 2026-07-26), in which case it's that
* Duplex's own rank being compared, not some notion of "Sunday's rank".
* When tomorrow's winner is genuinely the Sunday itself (`kind !==
* 'sanctoral'`), today's sufficiently high sanctoral rank
* (`duplex-2-classis`+) still claims the evening outright per the rubric's
* "Duplex always keeps both Vespers" rule handled by the
* `isAtLeast`-style fallback below, same floor as before, just no longer
* applied when tomorrow also has a real (comparable) sanctoral winner.
*/
export function resolveEveningDay(isoDate: string): LiturgicalDay {
const today = resolveDay(isoDate);
@@ -208,7 +234,19 @@ export function resolveEveningDay(isoDate: string): LiturgicalDay {
if (isMajorFixedFeastOfTheLord(tomorrow.date)) {
return tagVespersFrom(tomorrow);
}
let todayWins: boolean;
if (keepsOwnSecondVespers(today)) {
todayWins = true;
} else if (today.winner.kind === 'sanctoral' && tomorrow.winner.kind === 'sanctoral') {
todayWins = compareFeastClass(today.winner.rank, tomorrow.winner.rank) > 0;
} else if (today.winner.kind === 'sanctoral' && isAtLeast(today.winner.rank, 'duplex-2-classis')) {
todayWins = true;
} else {
todayWins = !hasFirstVespers(tomorrow);
}
if (todayWins) {
// Today wins outright, but tomorrow is still commemorated here as
// long as it's a real sanctoral winner — see
// commemorationOfDisplacedSuccessor's own doc comment for why this
@@ -216,8 +254,5 @@ export function resolveEveningDay(isoDate: string): LiturgicalDay {
const commemoration = commemorationOfDisplacedSuccessor(today.winner, tomorrow.winner);
return commemoration ? { ...today, commemorations: [...today.commemorations, commemoration] } : today;
}
if (!hasFirstVespers(tomorrow)) {
return today;
}
return anticipated(today, tomorrow);
}
@@ -11,6 +11,7 @@
# is correct.
id: all-saints-of-the-benedictine-order
name: "All Saints of the Order of St. Benedict"
nameLa: "Omnes Sancti Ordinis Sancti Benedicti"
rank: duplex-2-classis
common: proper-to-all-saints
propers: "all-saints-of-the-benedictine-order"
+1
View File
@@ -5,6 +5,7 @@
# was sourced).
id: all-saints
name: "All Saints"
nameLa: "Omnes Sancti"
rank: duplex-1-classis
common: proper-to-all-saints
propers: "all-saints"
@@ -9,6 +9,7 @@
# in this app's original stale exclusion note.
id: annunciation
name: "The Annunciation of the Blessed Virgin Mary"
nameLa: "Annuntiatio Beatæ Mariæ Virginis"
rank: duplex-1-classis
common: common-of-the-bvm
propers: "annunciation"
@@ -6,6 +6,7 @@
# collect and antiphon.
id: apparition-of-st-michael
name: "The Apparition of St. Michael the Archangel"
nameLa: "Apparitio Sancti Michaelis Archangeli"
rank: duplex-majus
common: common-of-an-angel
propers: "apparition-of-st-michael"
@@ -8,6 +8,7 @@
# immaculate-conception.yml's own Dec 8 Nocturn 2 content).
id: apparition-of-the-immaculate-bvm
name: "The Apparition of the Immaculate Blessed Virgin Mary (Our Lady of Lourdes)"
nameLa: "Apparitio Immaculatæ Beatæ Mariæ Virginis"
rank: duplex-majus
common: common-of-the-bvm
propers: "apparition-of-the-immaculate-bvm"
+1
View File
@@ -6,6 +6,7 @@
# specific block) rather than plain text, so left unpulled this pass.
id: assumption
name: "The Assumption of the Blessed Virgin Mary"
nameLa: "Assumptio Beatæ Mariæ Virginis"
rank: duplex-1-classis
common: common-of-the-bvm
propers: "assumption"
@@ -5,6 +5,7 @@
# identical text to Rome's, both intentionally duplicated rather than shared.
id: chair-of-st-peter-at-antioch
name: "The Chair of St. Peter at Antioch"
nameLa: "Cathedra Sancti Petri Antiochiæ"
rank: duplex-majus
common: common-of-an-apostle
propers: "chair-of-st-peter-at-antioch"
@@ -8,6 +8,7 @@
# since both Chair feasts share it).
id: chair-of-st-peter-at-rome
name: "The Chair of St. Peter at Rome"
nameLa: "Cathedra Sancti Petri Romæ"
rank: duplex-majus
common: common-of-an-apostle
propers: "chair-of-st-peter-at-rome"
@@ -5,6 +5,7 @@
# as its own record. Own proper collect and antiphon.
id: commemoration-of-st-paul
name: "Commemoration of St. Paul, Apostle"
nameLa: "Commemoratio Sancti Pauli Apostoli"
rank: duplex
common: common-of-an-apostle
propers: "commemoration-of-st-paul"
@@ -3,6 +3,7 @@
# proper collect and antiphon.
id: conversion-of-st-paul
name: "The Conversion of St. Paul, Apostle"
nameLa: "Conversio Sancti Pauli Apostoli"
rank: duplex-majus
common: common-of-an-apostle
propers: "conversion-of-st-paul"
@@ -2,14 +2,12 @@
# kalendar check (2025-08-29) -- winner outright, Duplex, over the stub
# St. Sabina (see st-sabina.yml). About St. John the Baptist's beheading,
# not a feast of the Lord/BVM, so kept in scope like his Nativity in June.
# "common-of-an-apostle" is a loose fit (he isn't one) but the closest
# existing category to "a major male saint honored with his own high
# feast" -- same judgment call as nativity-of-st-john-the-baptist.yml.
# Own proper collect and antiphon.
id: decollation-of-st-john-baptist
name: "The Beheading of St. John the Baptist"
nameLa: "Decollatio Sancti Joannis Baptistæ"
rank: duplex
common: common-of-an-apostle
common: common-of-a-martyr
propers: "decollation-of-st-john-baptist"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (08-29.txt) --
# [Rule]/[Rank] point to Commune/C2a instead, the same C-number family
@@ -15,6 +15,7 @@
# dedication-of-the-basilicas-of-ss-peter-and-paul.yml.
id: dedication-of-the-basilica-of-the-most-holy-savior
name: "Dedication of the Basilica of the Most Holy Savior (the Lateran)"
nameLa: "Dedicatio Basilicæ Sanctissimi Salvatoris"
rank: duplex-2-classis
common: common-of-a-dedication
propers: null
@@ -8,6 +8,7 @@
# file at this MM-DD slot (Sancti/11-18.txt) is empty.
id: dedication-of-the-basilicas-of-ss-peter-and-paul
name: "Dedication of the Basilicas of Ss. Peter and Paul"
nameLa: "Dedicatio Basilicarum Sanctorum Petri et Pauli Apostolorum"
rank: duplex
common: common-of-an-apostle
propers: null
@@ -15,6 +15,7 @@
# finding-of-the-holy-cross.yml's own reading content instead.
id: exaltation-of-the-holy-cross
name: "The Exaltation of the Holy Cross"
nameLa: "Exaltatio Sanctæ Crucis"
rank: duplex-majus
common: proper-to-the-finding-of-the-holy-cross
propers: "finding-of-the-holy-cross"
@@ -13,6 +13,7 @@
# Dec 26 antiphon reused verbatim, no substitution this time.
id: finding-of-st-stephen
name: "The Finding of St. Stephen, Protomartyr"
nameLa: "Inventio Sancti Stephani Protomartyris"
rank: semiduplex
common: common-of-a-martyr
propers: "finding-of-st-stephen"
@@ -13,6 +13,7 @@
# see exaltation-of-the-holy-cross.yml.
id: finding-of-the-holy-cross
name: "The Finding of the Holy Cross"
nameLa: "Inventio Sanctæ Crucis"
rank: duplex-2-classis
common: proper-to-the-finding-of-the-holy-cross
propers: "finding-of-the-holy-cross"
@@ -3,6 +3,7 @@
# Own proper collect, no unique antiphon of their own.
id: forty-holy-martyrs
name: "The Forty Holy Martyrs"
nameLa: "Quadraginta Sancti Martyres"
rank: semiduplex
common: common-of-several-martyrs
propers: "forty-holy-martyrs"
@@ -8,6 +8,7 @@
# scripture) not modeled per this app's established convention.
id: guardian-angels
name: "The Guardian Angels"
nameLa: "Sancti Angeli Custodes"
rank: duplex-majus
common: common-of-an-angel
propers: "guardian-angels"
@@ -6,6 +6,7 @@
# this pull has found. Own proper collect and antiphon.
id: holy-innocents
name: "The Holy Innocents, Martyrs"
nameLa: "Sancti Innocentes, Martyres"
rank: duplex-2-classis
common: common-of-several-martyrs
propers: "holy-innocents"
@@ -10,6 +10,7 @@
# after the relief of Vienna.
id: holy-name-of-mary
name: "The Holy Name of the Blessed Virgin Mary"
nameLa: "Sanctissimum Nomen Mariæ"
rank: duplex-majus
common: common-of-the-bvm
propers: "holy-name-of-mary"
@@ -4,6 +4,7 @@
# collect and antiphon.
id: immaculate-conception
name: "The Immaculate Conception of the Blessed Virgin Mary"
nameLa: "Immaculata Conceptio Beatæ Mariæ Virginis"
rank: duplex-1-classis
common: common-of-the-bvm
propers: "immaculate-conception"
@@ -8,6 +8,7 @@
# feast's own Lectio6, quoted in nocturn-readings).
id: maternity-of-the-bvm
name: "The Maternity of the Blessed Virgin Mary"
nameLa: "Maternitas Beatæ Mariæ Virginis"
rank: duplex-2-classis
common: common-of-the-bvm
propers: "maternity-of-the-bvm"
@@ -9,6 +9,7 @@
# Lectio6, quoted in nocturn-readings).
id: most-precious-blood
name: "The Most Precious Blood of Our Lord Jesus Christ"
nameLa: "Pretiosissimus Sanguis Domini Nostri Jesu Christi"
rank: duplex-1-classis
common: proper-to-the-most-precious-blood
propers: "most-precious-blood"
@@ -5,6 +5,7 @@
# wins outright over the stub St. Hadrian (see st-hadrian.yml).
id: nativity-bvm
name: "The Nativity of the Blessed Virgin Mary"
nameLa: "Nativitas Beatæ Mariæ Virginis"
rank: duplex-2-classis
common: common-of-the-bvm
propers: "nativity-bvm"
@@ -3,12 +3,22 @@
# classis, the highest rank pulled in this set. About St. John the
# Baptist's own nativity, not a feast of the Lord/BVM, so kept in the
# sanctoral store like the Conversion of St. Paul/Chair-of-Peter feasts
# despite the very high rank. Own proper collect and antiphon. "common-
# of-an-apostle" is a loose fit (he isn't one) but the closest existing
# category to "a major male saint honored with his own high feast" --
# worth reconsidering if a more precise category becomes worth adding.
# despite the very high rank. Own proper collect and antiphon.
#
# `common: common-of-an-apostle` is a placeholder, not a liturgical
# claim -- the reference engine assigns this feast no Commune at all
# (every [Rule] line in Sancti/06-24.txt and SanctiM/06-24.txt points to
# proper text, never a Commune category), so no better-fitting id exists
# either. Now verified-inert: every part this field could ever be
# consulted for (Matins invitatory/hymn/Nocturn psalmody, Vespers
# psalmody, the Lauds/Vespers bundle, Benedictus antiphon, collect, all
# four minor hours) has its own real proper text authored, so this field
# is never actually reached by any resolver (see
# data/hours/matins-psalmody-overrides/saints/nativity-of-st-john-the-baptist.yml's
# own comment).
id: nativity-of-st-john-the-baptist
name: "The Nativity of St. John the Baptist"
nameLa: "Nativitas Sancti Joannis Baptistæ"
rank: duplex-1-classis
common: common-of-an-apostle
propers: "nativity-of-st-john-the-baptist"
@@ -13,6 +13,7 @@
# collect/antiphon do.
id: octave-of-all-saints
name: "The Octave Day of All Saints"
nameLa: "Octava Omnium Sanctorum"
rank: duplex-majus
common: proper-to-all-saints
propers: "all-saints"
@@ -18,6 +18,7 @@
# vu-sanctoral-sweep-progress memory).
id: octave-of-ss-peter-and-paul
name: "The Octave Day of Ss. Peter and Paul, Apostles"
nameLa: "Octava Sanctorum Petri et Pauli, Apostolorum"
rank: duplex-majus
common: common-of-apostles
propers: "octave-of-ss-peter-and-paul"
@@ -0,0 +1,16 @@
# Read directly from the reference engine: Sancti/12-12n.txt --
# "Beatæ Mariæ Virginis de Guadalupe;;Duplex;;3;;vide C11" (Common of
# the BVM), the same rank family and `n` (national-calendar) sourcing as
# apparition-of-the-immaculate-bvm.yml (Our Lady of Lourdes, Feb 11) --
# not a canonization so the late-canonization deflation rule doesn't
# apply; the cultus itself is old enough for the project's period
# (Benedict XIV confirmed it 1754, Pius X named her Patroness of Latin
# America). Own proper Oratio and [Ant 1], same scope as the Lourdes
# feast -- no separate Vespers/Lauds antiphon set transcribed, Common of
# the BVM (C11) carries the rest when she wins outright.
id: our-lady-of-guadalupe
name: "Our Lady of Guadalupe"
nameLa: "Beata Maria Virgo de Guadalupe"
rank: duplex
common: common-of-the-bvm
propers: "our-lady-of-guadalupe"
@@ -10,6 +10,7 @@
# as our-lady-of-the-snows.yml/presentation-of-the-bvm.yml.
id: our-lady-of-mount-carmel
name: "Our Lady of Mount Carmel"
nameLa: "Beata Maria Virgo de Monte Carmelo"
rank: duplex-majus
common: common-of-the-bvm
propers: "our-lady-of-mount-carmel"
@@ -6,6 +6,7 @@
# own Nocturn 3, not transcribed here.
id: our-lady-of-ransom
name: "Our Lady of Ransom"
nameLa: "Beata Maria Virgo de Mercede"
rank: duplex-majus
common: common-of-the-bvm
propers: "our-lady-of-ransom"
@@ -16,6 +16,7 @@
# `propers: null` saint falling back to a shared Common.
id: our-lady-of-the-snows
name: "Our Lady of the Snows (Dedication of St. Mary Major)"
nameLa: "Dedicatio Sanctæ Mariæ ad Nives"
rank: duplex-majus
common: common-of-the-bvm
propers: null
@@ -19,6 +19,7 @@
# cross-referenced).
id: presentation-of-the-bvm
name: "The Presentation of the Blessed Virgin Mary"
nameLa: "Præsentatio Beatæ Mariæ Virginis"
rank: duplex-majus
common: common-of-the-bvm
propers: "presentation-of-the-bvm"
@@ -10,6 +10,7 @@
# of rubric-era markers.
id: purification
name: "The Purification of the Blessed Virgin Mary (Candlemas)"
nameLa: "Purificatio Beatæ Mariæ Virginis"
rank: duplex-2-classis
common: common-of-the-bvm
propers: "purification"
@@ -18,6 +18,7 @@
# is authored, isGospel: false, no citation field.
id: queenship-of-the-bvm
name: "The Queenship of the Blessed Virgin Mary"
nameLa: "Beata Maria Virgo Regina"
rank: duplex-2-classis
common: common-of-the-bvm
propers: "queenship-of-the-bvm"
@@ -9,6 +9,7 @@
# on John 19:25-27 and his own Epistle 25).
id: seven-sorrows-of-the-bvm
name: "The Seven Sorrows of the Blessed Virgin Mary"
nameLa: "Septem Dolores Beatæ Mariæ Virginis"
rank: duplex-2-classis
common: common-of-the-bvm
propers: "seven-sorrows-of-the-bvm"
@@ -3,6 +3,7 @@
# Simplex. Own proper collect, no unique antiphon of their own.
id: ss-abdon-and-sennen
name: "Ss. Abdon and Sennen, Martyrs"
nameLa: "Sancti Abdon et Sennes, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-abdon-and-sennen"
@@ -9,6 +9,7 @@
# caveat as every other stub. No text pulled at all yet.
id: ss-alexander-eventius-theodulus-and-juvenal
name: "Ss. Alexander I, Eventius, and Theodulus, Martyrs, and Juvenal, Bishop and Confessor"
nameLa: "Sancti Alexander, Eventius et Theodulus, Martyres, et Sanctus Juvenalis, Episcopus et Confessor"
rank: simplex
common: common-of-several-martyrs
propers: "ss-alexander-eventius-theodulus-and-juvenal"
@@ -3,6 +3,7 @@
# proper collect and antiphon.
id: ss-basilides-cyrinus-nabor-and-nazarius
name: "Ss. Basilides, Cyrinus, Nabor, and Nazarius, Martyrs"
nameLa: "Sancti Basilides, Cyrinus, Nabor et Nazarius, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-basilides-cyrinus-nabor-and-nazarius"
@@ -7,6 +7,7 @@
# proper collect, no unique antiphon of their own.
id: ss-chrysanthus-and-daria
name: "Ss. Chrysanthus and Daria, Martyrs"
nameLa: "Sancti Chrysanthus et Daria, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-chrysanthus-and-daria"
@@ -3,6 +3,7 @@
# No unique Oratio/Antiphon -- Common of Several Martyrs only.
id: ss-cletus-and-marcellinus
name: "Ss. Cletus and Marcellinus, Popes and Martyrs"
nameLa: "Sancti Cletus et Marcellinus, Papæ et Martyres"
rank: semiduplex
common: common-of-several-martyrs
propers: null
@@ -8,6 +8,7 @@
# raw file at this MM-DD slot (Sancti/09-16.txt) is empty.
id: ss-cornelius-and-cyprian
name: "Ss. Cornelius, Pope, and Cyprian, Bishop, Martyrs"
nameLa: "Sanctus Cornelius, Papa, et Sanctus Cyprianus, Episcopus, Martyres"
rank: semiduplex
common: common-of-several-martyrs
propers: null
@@ -3,6 +3,7 @@
# proper collect, no unique antiphon of their own.
id: ss-cosmas-and-damian
name: "Ss. Cosmas and Damian, Martyrs"
nameLa: "Sancti Cosmas et Damianus, Martyres"
rank: semiduplex
common: common-of-several-martyrs
propers: "ss-cosmas-and-damian"
@@ -6,6 +6,7 @@
# Justina instead, a distinct legend.
id: ss-cyprian-and-justina
name: "Ss. Cyprian and Justina, Martyrs"
nameLa: "Sanctus Cyprianus et Sancta Justina, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-cyprian-and-justina"
@@ -3,6 +3,7 @@
# proper collect, no unique antiphon of their own.
id: ss-cyriacus-largus-and-smaragdus
name: "Ss. Cyriacus, Largus, and Smaragdus, Martyrs"
nameLa: "Sancti Cyriacus, Largus et Smaragdus, Martyres"
rank: semiduplex
common: common-of-several-martyrs
propers: "ss-cyriacus-largus-and-smaragdus"
@@ -10,6 +10,7 @@
# transcribed, per the bare-cross-reference convention.
id: ss-cyril-and-methodius
name: "Ss. Cyril and Methodius, Bishops and Confessors"
nameLa: "Sancti Cyrillus et Methodius, Episcopi et Confessores"
rank: duplex
common: common-of-a-confessor-bishop
propers: "ss-cyril-and-methodius"
@@ -7,6 +7,7 @@
# is correct.
id: ss-dionysius-and-companions
name: "St. Dionysius, Bishop, and Companions, Martyrs"
nameLa: "Sanctus Dionysius, Episcopus, et Socii, Martyres"
rank: semiduplex
common: common-of-a-martyr-bishop
propers: "ss-dionysius-and-companions"
@@ -10,6 +10,7 @@
# placeholders.
id: ss-euphemia-lucia-and-geminianus
name: "Ss. Euphemia, Lucy, and Geminianus, Martyrs"
nameLa: "Sanctæ Euphemia et Lucia, et Sanctus Geminianus, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-euphemia-lucia-and-geminianus"
@@ -3,6 +3,7 @@
# shared by two martyrs. Own proper collect, no unique antiphon of their own.
id: ss-fabian-and-sebastian
name: "Ss. Fabian and Sebastian, Martyrs"
nameLa: "Sancti Fabianus et Sebastianus, Martyres"
rank: duplex
common: common-of-several-martyrs
propers: "ss-fabian-and-sebastian"
@@ -4,6 +4,7 @@
# Oratio/Antiphon in the source -- Common of Several Martyrs only.
id: ss-faustinus-and-jovita
name: "Ss. Faustinus and Jovita, Martyrs"
nameLa: "Sancti Faustinus et Jovita, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: null
@@ -1,16 +1,22 @@
# Verified against Divinum Officium (Monastic Tridentinum 1617), live
# kalendar check on a clean weekday (2027-08-30, a Monday) -- wins outright
# at Simplex. On a Saturday this date is instead outranked by the generic
# "Sanctae Mariae Sabbato" votive (confirmed re-checking 2025-08-30, a
# Saturday, where the votive won instead) -- an artifact of the tested
# weekday, same pattern as St. Stephen I (Aug 2) this month and Ss.
# Marcellinus/Peter/Erasmus in June. No unique collect: the raw file at
# this MM-DD slot (Sancti/08-30.txt) is actually St. Rose of Lima's (a
# much later, 17th-century addition to this filename slot) -- a live
# prayTertia query for the Monastic 1617 track returns no Oratio at all,
# confirming propers: null is correct.
# at Simplex against a bare weekday. On a Saturday this date is instead
# outranked by the generic "Sanctae Mariae Sabbato" votive (confirmed
# re-checking 2025-08-30, a Saturday, where the votive won instead) -- an
# artifact of the tested weekday, same pattern as St. Stephen I (Aug 2)
# this month and Ss. Marcellinus/Peter/Erasmus in June. No unique collect
# of its own here on the Monastic 1617 track (a live prayTertia query for
# that track returns no Oratio at all): the raw file at this MM-DD slot
# (Sancti/08-30.txt) actually belongs to St. Rose of Lima, a much later
# (17th-century) addition to this filename slot on the general
# Tridentine/Divino Afflatu calendar -- see st-rose-of-lima.yml, now
# authored and the day's outright winner (semiduplex > simplex) per
# sanctoral-calendar.yml. Felix and Adauctus's own real content survives
# at the suffixed sibling file (Sancti/08-30o.txt) and is kept here as a
# full commemoration, not suppressed, per collision.ts.
id: ss-felix-and-adauctus
name: "Ss. Felix and Adauctus, Martyrs"
nameLa: "Sancti Felix et Adauctus, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-felix-and-adauctus"
@@ -9,6 +9,7 @@
# text pulled at all yet.
id: ss-felix-simplicius-faustinus-and-beatrix
name: "Ss. Felix, Simplicius, Faustinus, and Beatrix, Martyrs"
nameLa: "Sancti Felix, Simplicius, Faustinus et Beatrix, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-felix-simplicius-faustinus-and-beatrix"
@@ -3,6 +3,7 @@
# proper collect, no unique antiphon of their own.
id: ss-gervase-and-protase
name: "Ss. Gervase and Protase, Martyrs"
nameLa: "Sancti Gervasius et Protasius, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-gervase-and-protase"
@@ -6,6 +6,7 @@
# ss-marcellinus-peter-and-erasmus.yml.
id: ss-hippolytus-and-cassian
name: "Ss. Hippolytus and Cassian, Martyrs"
nameLa: "Sancti Hippolytus et Cassianus, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-hippolytus-and-cassian"
@@ -5,6 +5,7 @@
# antiphon.
id: ss-john-and-paul
name: "Ss. John and Paul, Martyrs"
nameLa: "Sancti Joannes et Paulus, Martyres"
rank: semiduplex
common: common-of-several-martyrs
propers: "ss-john-and-paul"
@@ -4,6 +4,7 @@
# an artifact of the tested year. rank/propers unverified placeholders.
id: ss-machabees
name: "Ss. Machabees, Martyrs"
nameLa: "Sancti Machabæi, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-machabees"
@@ -5,6 +5,7 @@
# their own.
id: ss-marcellinus-peter-and-erasmus
name: "Ss. Marcellinus, Peter, and Erasmus, Bishop, Martyrs"
nameLa: "Sancti Marcellinus et Petrus, et Sanctus Erasmus, Episcopus, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-marcellinus-peter-and-erasmus"
@@ -7,6 +7,7 @@
# author" scope for merely-commemorated saints.
id: ss-marius-martha-audifax-and-abachum
name: "Ss. Marius, Martha, Audifax, and Abachum, Martyrs"
nameLa: "Sancti Marius, Martha, Audifax et Abachum, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-marius-martha-audifax-and-abachum"
@@ -3,6 +3,7 @@
# proper collect, no unique antiphon of their own.
id: ss-mark-and-marcellian
name: "Ss. Mark and Marcellian, Martyrs"
nameLa: "Sancti Marcus et Marcellianus, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-mark-and-marcellian"
@@ -8,6 +8,7 @@
# August's entries.
id: ss-maurice-and-companions
name: "Ss. Maurice and Companions, Martyrs"
nameLa: "Sanctus Mauritius et Socii, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-maurice-and-companions"
@@ -9,6 +9,7 @@
# text pulled at all yet.
id: ss-nabor-and-felix
name: "Ss. Nabor and Felix, Martyrs"
nameLa: "Sancti Nabor et Felix, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-nabor-and-felix"
@@ -6,6 +6,7 @@
# antiphon of their own.
id: ss-nazarius-celsus-victor-and-innocent
name: "Ss. Nazarius and Celsus, Martyrs, St. Victor I, Pope and Martyr, and St. Innocent I, Pope and Confessor"
nameLa: "Sancti Nazarius et Celsus, Martyres, Sanctus Victor I, Papa et Martyr, et Sanctus Innocentius I, Papa et Confessor"
rank: semiduplex
common: common-of-several-martyrs
propers: "ss-nazarius-celsus-victor-and-innocent"
@@ -3,6 +3,7 @@
# Own proper collect, no unique antiphon of their own.
id: ss-nereus-achilleus-domitilla-and-pancras
name: "Ss. Nereus, Achilleus, Domitilla, and Pancras, Martyrs"
nameLa: "Sancti Nereus, Achilleus et Pancratius, et Sancta Domitilla, Martyres"
rank: semiduplex
common: common-of-several-martyrs
propers: "ss-nereus-achilleus-domitilla-and-pancras"
@@ -5,6 +5,7 @@
# benedictus-antiphon-common-of-several-women-martyrs.yml).
id: ss-perpetua-and-felicity
name: "Ss. Perpetua and Felicity, Martyrs"
nameLa: "Sanctæ Perpetua et Felicitas, Martyres"
rank: duplex
common: common-of-several-women-martyrs
propers: null
@@ -5,6 +5,7 @@
# duplicated here rather than shared, see ss-peter-and-paul-antiphon.yml.
id: ss-peter-and-paul
name: "Ss. Peter and Paul, Apostles"
nameLa: "Sancti Petrus et Paulus, Apostoli"
rank: duplex-1-classis
common: common-of-an-apostle
propers: "ss-peter-and-paul"
@@ -3,6 +3,7 @@
# classis. Own proper collect and antiphon.
id: ss-philip-and-james
name: "Ss. Philip and James, Apostles"
nameLa: "Sancti Philippus et Jacobus, Apostoli"
rank: duplex-2-classis
common: common-of-an-apostle
propers: "ss-philip-and-james"
@@ -10,6 +10,7 @@
# treatment as any other saint whose own file just points at the Common.
id: ss-placid-and-companions
name: "Ss. Placid and Companions, Martyrs"
nameLa: "Sanctus Placidus et Socii, Martyres"
rank: duplex-2-classis
common: common-of-several-martyrs
propers: null
@@ -5,6 +5,7 @@
# antiphon of their own.
id: ss-primus-and-felicianus
name: "Ss. Primus and Felicianus, Martyrs"
nameLa: "Sancti Primus et Felicianus, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-primus-and-felicianus"
@@ -6,6 +6,7 @@
# ss-marcellinus-peter-and-erasmus.yml.
id: ss-protus-and-hyacinth
name: "Ss. Protus and Hyacinth, Martyrs"
nameLa: "Sancti Protus et Hyacinthus, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-protus-and-hyacinth"
@@ -3,6 +3,7 @@
# Own proper collect, no unique antiphon of their own.
id: ss-seven-brothers-rufina-and-secunda
name: "Ss. Seven Brothers, Martyrs, and Ss. Rufina and Secunda, Virgins and Martyrs"
nameLa: "Septem Fratres, Martyres, et Sanctæ Rufina et Secunda, Virgines et Martyres"
rank: semiduplex
common: common-of-several-martyrs
propers: "ss-seven-brothers-rufina-and-secunda"
@@ -6,6 +6,7 @@
# a group of founders, not the saints' own text.
id: ss-seven-founders-of-the-servite-order
name: "Ss. Seven Holy Founders of the Order of the Servants of the Blessed Virgin Mary"
nameLa: "Septem Sancti Fundatores Ordinis Servorum Beatæ Mariæ Virginis"
rank: duplex
common: common-of-several-confessors
propers: "ss-seven-founders-of-the-servite-order"
@@ -3,6 +3,7 @@
# alone. Own proper collect, no unique antiphon of their own.
id: ss-simon-and-jude
name: "Ss. Simon and Jude, Apostles"
nameLa: "Sancti Simon et Judas, Apostoli"
rank: duplex-2-classis
common: common-of-an-apostle
propers: "ss-simon-and-jude"
@@ -6,6 +6,7 @@
# this set. No unique Oratio/Antiphon -- Common of Several Martyrs only.
id: ss-soter-and-caius
name: "Ss. Soter and Caius, Popes and Martyrs"
nameLa: "Sancti Soter et Cajus, Papæ et Martyres"
rank: semiduplex
common: common-of-several-martyrs
propers: null
@@ -3,6 +3,7 @@
# proper collect, no unique antiphon of their own.
id: ss-symphorosa-and-seven-sons
name: "Ss. Symphorosa and Her Seven Sons, Martyrs"
nameLa: "Sancta Symphorosa et Septem Filii Ejus, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-symphorosa-and-seven-sons"
@@ -6,6 +6,7 @@
# ss-marcellinus-peter-and-erasmus.yml.
id: ss-tiburtius-and-susanna
name: "Ss. Tiburtius and Susanna, Martyrs"
nameLa: "Sanctus Tiburtius et Sancta Susanna, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-tiburtius-and-susanna"
@@ -4,6 +4,7 @@
# their own.
id: ss-tiburtius-valerian-and-maximus
name: "Ss. Tiburtius, Valerian, and Maximus, Martyrs"
nameLa: "Sancti Tiburtius, Valerianus et Maximus, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-tiburtius-valerian-and-maximus"
@@ -6,6 +6,7 @@
# confirming propers: null is correct.
id: ss-tryphon-and-companions
name: "Ss. Tryphon, Respicius, and Nympha, Martyrs"
nameLa: "Sancti Tryphon, Respicius et Nympha, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-tryphon-and-companions"
@@ -4,6 +4,7 @@
# tested year. rank/propers unverified placeholders.
id: ss-twelve-brothers
name: "Ss. Twelve Brothers, Martyrs"
nameLa: "Duodecim Fratres, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-twelve-brothers"
@@ -15,6 +15,7 @@
# anyway (resolveOfficeWinner only ever returns the day's actual winner).
id: ss-ursula-and-companions
name: "Ss. Ursula and Companions, Virgins and Martyrs"
nameLa: "Sancta Ursula et Sociæ, Virgines et Martyres"
rank: simplex
common: common-of-several-martyrs
propers: null
@@ -3,6 +3,7 @@
# proper collect, no unique antiphon of their own.
id: ss-vincent-and-anastasius
name: "Ss. Vincent and Anastasius, Martyrs"
nameLa: "Sancti Vincentius et Anastasius, Martyres"
rank: semiduplex
common: common-of-several-martyrs
propers: "ss-vincent-and-anastasius"
@@ -3,6 +3,7 @@
# proper collect, no unique antiphon of their own.
id: ss-vitus-modestus-and-crescentia
name: "Ss. Vitus, Modestus, and Crescentia, Martyrs"
nameLa: "Sancti Vitus, Modestus et Crescentia, Martyres"
rank: simplex
common: common-of-several-martyrs
propers: "ss-vitus-modestus-and-crescentia"
+1
View File
@@ -8,6 +8,7 @@
# saint's own original authoring, hence the gap).
id: st-agatha
name: "St. Agatha, Virgin and Martyr"
nameLa: "Sancta Agatha, Virgo et Martyr"
rank: semiduplex
common: common-of-a-virgin-martyr
propers: "st-agatha"
@@ -7,6 +7,7 @@
# author" scope for merely-commemorated saints.
id: st-agnes-secunda
name: "St. Agnes, Virgin and Martyr, Second Feast"
nameLa: "Sancta Agnes, Virgo et Martyr (Secundo)"
rank: simplex
common: common-of-a-virgin-martyr
propers: "st-agnes-secunda"
+1
View File
@@ -4,6 +4,7 @@
# -- see st-agnes-secunda.yml.
id: st-agnes
name: "St. Agnes, Virgin and Martyr"
nameLa: "Sancta Agnes, Virgo et Martyr"
rank: duplex
common: common-of-a-virgin-martyr
propers: "st-agnes"
@@ -9,6 +9,7 @@
# itself a cross-reference to C4).
id: st-albert-the-great
name: "St. Albert the Great, Bishop, Confessor and Doctor of the Church"
nameLa: "Sanctus Albertus Magnus, Episcopus, Confessor et Ecclesiæ Doctor"
rank: duplex
common: common-of-a-confessor-doctor
propers: "st-albert-the-great"
+1
View File
@@ -5,6 +5,7 @@
# so no antiphon is recorded -- Common of a Confessor only.
id: st-alexius
name: "St. Alexius, Confessor"
nameLa: "Sanctus Alexius, Confessor"
rank: simplex
common: common-of-a-confessor
propers: null
@@ -8,6 +8,7 @@
# -- ignored per the untagged-block convention.
id: st-aloysius-gonzaga
name: "St. Aloysius Gonzaga, Confessor"
nameLa: "Sanctus Aloysius Gonzaga, Confessor"
rank: duplex
common: common-of-a-confessor
propers: "st-aloysius-gonzaga"

Some files were not shown because too many files have changed in this diff Show More