Compare commits

..

391 Commits

Author SHA1 Message Date
will 1cdb9f9822 Trim TODO.md's Open Work section down to actual open items
Deploy / deploy (push) Successful in 1m53s
Cut the intro/priority blurb, dropped the epilogue prose after
"none currently open" (that detail lives in the dated log below),
stripped New Features down to just the features, and removed the
Known Simplifications list entirely since it isn't open work — that
detail already lives as comments in temporal-categories.yml.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGsATZvfnpQ81HQuoF5JuL
2026-09-05 17:41:41 -04:00
will 9963a9b72e Correct stale "Canticum Ieremiæ" premise, update TODO.md backlog
Deploy / deploy (push) Successful in 2m0s
The 2026-08-28 note blocking Lauds' Common-of-a-Dedication file was
based on a mis-targeted live query (plain ferial Thursday's own output,
not actually votive=C8) rather than a real content gap. Fix the
Vespers file's header comment and close out the corresponding TODO.md
backlog entries with a dated log entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGsATZvfnpQ81HQuoF5JuL
2026-09-05 16:22:12 -04:00
will 989a63c05e Author Common-of-a-Dedication Lauds psalmody, closing 10/10 sweep
Live-verified the real scheme against Divinum Officium (Monastic
Tridentinum 1617, votive=C8) across all 7 weekdays: proper antiphons on
Ps 50/5/35, plus a canticle slot whose antiphon is fixed to this Common
but whose underlying text follows the plain ferial weekday rotation
(all 7 already authored). Required making
LaudsPsalmodyOverride.canticle.id optional and having resolvePsalmody
splice in the day's own ferial canticle when a category omits it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGsATZvfnpQ81HQuoF5JuL
2026-09-05 16:22:02 -04:00
will cf0ecdf4c5 Close latent Matins Gospel-homily dedup gap
Deploy / deploy (push) Successful in 2m24s
When a Gospel+homily pair is folded into one pool entry, the homily's
own text was never registered in seenReadingText -- only the Gospel
pericope's text was. Not live for any date today, but the same
structural mistake already fixed once for two ids pooling the same
Gregory the Great excerpt: a later pooled id whose solo lesson happens
to share a homily's exact text would render as a visible duplicate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6rG4RTNqLFYcyjbCGkPFs
2026-09-05 14:08:36 -04:00
will 9d671751d9 Annotate Matins octave readings with their occasion too
The octave-reading branch never called withOccasion/occasionName at
all, so an active octave's own reading always showed a bare source
even when pooled alongside other content on the same day -- the same
missing-annotation gap just fixed for plain-Sunday and saint/temporal
readings, in a fourth branch that predates that helper. octave-readings'
source is bilingual ({la, en}), unlike nocturn-readings' plain string,
so the occasion suffix is appended to both language slots directly
rather than routed through withOccasion itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6rG4RTNqLFYcyjbCGkPFs
2026-09-05 14:08:03 -04:00
will ff94000f48 Annotate plain-Sunday-sourced Matins readings with their occasion
occasionName() only recognized saints, named temporal feasts, and Ember
days, so any reading pooled under a plain governing-Sunday id
(post-pentecost-15, month-week-092, etc.) -- the most common id shape
in the whole pool -- silently got no "(for ...)" annotation. This is
why Sept 6's Gregory the Great homily and its paired Augustine Gospel
homily showed up with no indication of why they were present. Falls
back to the same getDayLabel() the day's own heading already uses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6rG4RTNqLFYcyjbCGkPFs
2026-09-05 14:07:27 -04:00
will 28b29c71d2 Author the remaining 6 Matins Nocturn 3 gaps found last commit
Deploy / deploy (push) Successful in 2m17s
Unlike the ~110 files fixed previously (a homily present with no
paired Gospel), these 6 had no Nocturn 3 at all: nativity-bvm.yml
(Matt 1:1-16 + St. Jerome), ss-cletus-and-marcellinus.yml/
ss-soter-and-caius.yml (Common-of-Popes, Matt 16:13-19 + Leo's Sermon
2 on his own Election, reused from st-martin-i.yml), ss-cornelius-and-
cyprian.yml (Common-of-Martyrs, Luke 21:9-19 + Gregory's Homily 35),
ss-seven-brothers-rufina-and-secunda.yml (a proper override, Matt
12:46-50 + Gregory's Homily 3), and st-anacletus.yml (an alternate
"rubrica 1570 aut rubrica 1617" variant carrying real content the
file's modern-rubric default lacks entirely -- Luke 14:26-33 +
Gregory's Homily 37, English composed directly from the Latin where
no reference-engine translation lines up with this Lectio split).

npm test (2029 passed) and tsc --noEmit both pass; live-verified via
resolveOrdo on all 6 saints' real dates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKC2QQGLYo2UTiDjDyWEkW
2026-09-05 12:33:03 -04:00
will 4bd05e68f0 Author missing Matins Gospel pericopes across nocturn-readings store
Deploy / deploy (push) Successful in 1m56s
~110 of 355 nocturn-readings files had a patristic homily with no
paired Gospel-pericope text -- the homily discussing a Gospel that was
never itself transcribed into the file. Closed in two waves (the first
pass's source-string regex missed many real cases where the citation
lived only in a header comment or wasn't named at all), each entry
sourced from divinum-officium-reference's own Missale files (following
Sancti/Commune redirects where a feast reuses another's Gospel) and
verified per file rather than assumed -- several candidates turned out
to be genuine Nocturn 2 content or Common-of-saints cross-references
with no proper Gospel of their own, correctly left untouched.

Includes both live-reported gaps: St. Joseph of Cupertino's own Bk. II
Homily 38 (Matt 22:1-14) and the Vigil of St. Matthew's Ambrose homily
(Luke 5:27-32).

TODO.md logs the closed item plus a new, distinct follow-up: a handful
of files with no Nocturn 3 content authored at all (not just an
unpaired Gospel), found along the way.

npm test (2029 passed) and tsc --noEmit both pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKC2QQGLYo2UTiDjDyWEkW
2026-09-05 12:13:55 -04:00
will 65a1f359e6 Fix Matins reading-pool ordering/leak bugs and redundant day-label commemoration
- nocturnReadingIds no longer lets a demoted commemoration's temporal id
  (equal to the plain governing-Sunday temporalId) sneak a Sunday's own
  homily into an unrelated weekday's pool via the commemorations loop —
  a second code path than the one a prior fix gated, affecting 18
  days/year.
- Reading pool order now follows id priority (winner first, then every
  temporal commemoration, then every sanctoral commemoration) instead of
  raw nocturn-tag/array order, matching the day label's own
  temporal-then-sanctoral convention.
- gospelReadingPart now falls back to the paired homily's own responsory
  when the Gospel entry has none of its own, instead of silently
  dropping it.
- Patristic source labels now show "(for <occasion>)" so a reader can
  tell which occasion a pooled reading belongs to on a multi-reading day.
- collectCommemorations no longer restates the plain generic temporal
  label (e.g. "Saturday in the 15th week after Trinity") once anything
  else is already being celebrated — general, not Ember-specific.
  Updated 4 day-label tests that encoded the old behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKC2QQGLYo2UTiDjDyWEkW
2026-09-05 12:13:41 -04:00
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
will dc07ca689f Add the Most Holy Name of Jesus (Jan 2) as a proper temporal feast
Deploy / deploy (push) Successful in 1m21s
Historically a floating Sunday (Jan 2-5, fixed to Jan 2 when no Sunday
falls there); per direct instruction this is instead fixed to Jan 2
every year for simplicity. Adds the feast's collect, Magnificat/
Benedictus antiphons, full Lauds psalmody, capitulum, the three-part
"Jesu dulcis memoria" hymn cycle (Vespers/Matins/Lauds), the Invitatory
antiphon, versicles, and two Matins nocturn readings (both St. Bernard
of Clairvaux), sourced from Tempora/Nat2-0.txt. Reuses the `fixed-date`
anchor kind added in the prior commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 16:28:00 -04:00
will f56fb3bd64 Add the Circumcision of Our Lord (Jan 1) as a proper temporal feast
Jan 1 previously fell through to generic Christmastide-feria content with
no identity of its own, despite being Christmas's own Octave Day. Adds a
`fixed-date` anchor kind to calendar/movable-feasts.ts's MovableAnchor
(the existing easter/nth-sunday kinds can't express "always Jan 1"),
plus the feast's collect, Magnificat/Benedictus antiphons, full Lauds
psalmody, capitulum, versicle, and two Matins nocturn readings (Leo the
Great, Ambrose), sourced from Sancti/01-01.txt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 16:27:39 -04:00
will 4bbd78b1fd Model Ascension, Corpus Christi, and the Sacred Heart as temporal feasts
Deploy / deploy (push) Successful in 1m23s
Adds pre-1955 TemporalFeastRecords (rank duplex-1-classis, real octaves)
for the three "feasts of the Lord" that were previously only referenced
as bare date offsets: Corpus Christi/Sacred Heart's own days already
couldn't lose to a sanctoral candidate, but with no record the day fell
back to the generic Trinity-week feria collect and never displayed its
real name; Ascension had no protection at all against an ordinary saint
winning it outright. Sacred Heart's own record carries the
forbidsSuccessorCommemoration flag added in the prior commit, refusing to
commemorate Most Precious Blood on the rare late-Easter date they fall
adjacent.

octave.wins is set to duplex on all three, matching Pentecost's own
verified threshold, despite a known conflict with a previously
live-verified case (St. Ubald winning First Vespers within Ascension's
own octave) -- kept per direct instruction; the affected tests (Ubald,
St. Margaret of Scotland) were updated to assert the new behavior, and
the tradeoff is documented in ascension.yml and TODO.md. A handful of
unrelated test fixtures that happened to land inside these new octave
windows were moved to nearby clean dates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 15:21:26 -04:00
will 2f7b421558 Label Vespers cross-day commemorations and add feast-side privilege
A commemoration that only exists because Vespers/Compline crossed a day
boundary now renders as "(of today)"/"(of tomorrow)" in English and the
real Divino Afflatu "de præcedenti"/"de sequenti" in Latin, instead of
showing silently with no indication it's a cross-day artifact.

Also adds forbidsSuccessorCommemoration to SaintRecord and
TemporalFeastRecord: a feast can now refuse to commemorate whatever wins
the next day at its own kept Second Vespers, the feast-side mirror of the
day-side privilege calendar/commemorations.ts already modeled. Left unset
everywhere for now (a no-op) -- it exists so the next commit's Sacred
Heart record has somewhere to carry the real, live-sourced exclusion
against Most Precious Blood.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 15:20:10 -04:00
will 9067faf1fd Author Matins hymns for the categories/saints from this session's Lauds/Vespers pass
Deploy / deploy (push) Successful in 1m22s
resolveMatinsHymn (hours/matins.ts) is a separate mechanism from
resolveOfficeBundle, with its own fallback chain (proper -> active
octave -> Common via SaintRecord.common -> season -> ferial). Only the
Confessor family and Apostle had a matins-hymn-<id>.yml before this
pass; everything else fell through to season/ferial at Matins
specifically, even after the Lauds/Vespers fixes.

Live-verified and authored: common-of-several-martyrs ("Ætérna Christi
múnera, Et Mártyrum victórias"), common-of-an-abbot and
common-of-several-confessors (= Confessor-Not-Bishop's hymn, same label
finding as Lauds/Vespers), common-of-a-virgin-martyr/-virgin ("Vírginis
Proles"), common-of-a-holy-woman/-widow (a genuinely shorter closing-two-
stanzas form, "Hujus obténtu, Deus alme"), Guardian Angels/St.
Michael/St. Gabriel/St. Raphael (reused from St. Michael's Apparition,
confirmed via SanctiM/05-08.txt's own cross-reference), common-of-the-
bvm ("Quem terra, pontus, ǽthera"), Finding/Exaltation of the Holy Cross
("Pange, lingua, gloriósi," the Nativity-themed other half of the same
poem as Lauds' own hymn), and the Transfiguration (byte-identical to its
own Vespers hymn).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 12:10:52 -04:00
will 8efde13dce Import All Saints of the Benedictine Order's own proper Lauds/Vespers content
Deploy / deploy (push) Successful in 1m25s
Last of the 12-saint individual-Proprio list this session. Live-verified
against Divinum Officium (Monastic Tridentinum 1617), 2025-11-13 (clean
year): own chapter (Isa 8:18, "Ecce ego et púeri mei") and own hymn
("Salvéte, cedri Líbani," same text both hours); responsory and versicle
byte-identical to Common-of-Several-Martyrs's own already-authored
content.

This closes the individual-saint Proprio pass: 10 real saints/feasts
imported, 2 flagged stubs (Ss. Alexander etc., Ss. Machabees) confirmed
to need no new content. Combined with the 10 Common categories and the
Guardian-Angels/Archangels fix earlier this session, resolveOfficeBundle
now has real, live-verified Lauds/Vespers content for every gap the
original audit found.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 11:04:56 -04:00
will ebc5459d5e Import the Transfiguration's own proper Lauds/Vespers content
Deploy / deploy (push) Successful in 1m22s
Live-verified against Divinum Officium (Monastic Tridentinum 1617),
2026-08-06 (fixed date). Fully proper feast: own chapter (Phil 3:20-21),
own distinct responsory/hymn/versicle at each hour ("Amor Jesu
dulcíssime" Lauds, "Quicúmque Christum quǽritis" Vespers), with real
Alleluias baked into the responsories.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 11:01:29 -04:00
will e19762ed17 Import Finding/Exaltation of the Holy Cross's own proper Lauds/Vespers content
Deploy / deploy (push) Successful in 1m18s
Fully proper feast, live-verified against Divinum Officium (Monastic
Tridentinum 1617), 2027-05-03 for the Finding (its own 2026 date
collides with a privileged Sunday) and 2026-09-14 for the Exaltation.
Own chapter (Phil 2:5-7), own distinct responsory/hymn/versicle at each
hour -- Lauds hymn "Lustris sex qui jam peráctis," Vespers hymn "Vexílla
Regis pródeunt," both with real Alleluias baked into their own text
(confirmed permanent via the Vespers hymn's own "In hoc Pascháli gáudio"
line, not seasonal contamination).

Authored under two ids (finding-of-the-holy-cross and proper-to-the-
finding-of-the-holy-cross): Exaltation's own saint file sets common:
proper-to-the-finding-of-the-holy-cross, a different id than its propers
field (a separate day-collect/antiphon lookup) -- without the second
copy, resolveOfficeBundle fell through to the ferial default for her.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 10:58:07 -04:00
will 5ec3394644 Import the Purification's own proper Lauds/Vespers chapter
Deploy / deploy (push) Successful in 1m18s
Live-verified against Divinum Officium (Monastic Tridentinum 1617),
2026-02-02 (fixed date, always winnable): only the chapter is genuinely
her own (Mal 3:1, "Ecce ego mitto Angelum meum"). Responsory, both
hymns, and both versicles are all byte-identical to Common-of-the-BVM's
own already-authored content -- duplicated under her own id.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 10:20:13 -04:00
will 545c11afe1 Import the Annunciation's own proper Lauds/Vespers content
Deploy / deploy (push) Successful in 1m19s
Live-verified against Divinum Officium (Monastic Tridentinum 1617),
2026-03-25 (fixed date, always winnable). Own chapter (Isa 7:14-15,
"Ecce virgo concípiet") and own versicle ("Ave, María, grátia plena")
at both hours, own Vespers responsory ("Ángelus Dómini * Nuntiávit
Maríæ," the opening of the Angelus). Lauds responsory and both hours'
hymns turn out byte-identical to Common-of-the-BVM's own already-
authored content -- reused verbatim.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 10:18:06 -04:00
will 9ce89c0312 Import St. Agnes's own proper Lauds/Vespers chapter
Deploy / deploy (push) Successful in 1m18s
Live-verified against Divinum Officium (Monastic Tridentinum 1617),
2029-01-21: byte-identical to St. Agatha's own override (Sir 51:1-3).
Confirmed a real, saint-specific shared override between these two
ancient Roman virgin martyrs specifically, not a general Virgin-Martyr
rank-tier variant -- St. Lucy (also a Canon-of-the-Mass virgin martyr)
uses the plain Common chapter on her own clean day. Responsory, hymn,
and versicle reuse Common-of-a-Virgin-Martyr's own content.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 10:14:12 -04:00
will 186ce63303 Import St. Agatha's own proper Lauds/Vespers chapter
Deploy / deploy (push) Successful in 1m18s
Live-verified against Divinum Officium (Monastic Tridentinum 1617),
2026-02-05: only the chapter is genuinely her own (Sir 51:1-3,
"Confitébor tibi, Dómine, Rex"). Responsory, hymn, and versicle are all
byte-identical to Common-of-a-Virgin-Martyr's own already-authored
content -- duplicated under her own id.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 10:11:34 -04:00
will 9c741545d8 Import Commemoration of St. Paul's own proper Lauds/Vespers chapter
Deploy / deploy (push) Successful in 1m18s
Live-verified against Divinum Officium (Monastic Tridentinum 1617),
2035-06-30 (clean year): only the chapter is genuinely his own (2 Tim
4:7-8, "Bonum certámen certávi" -- Paul's own "I have fought the good
fight"). Responsory, hymn, and versicle are all byte-identical to
Common-of-an-Apostle's own already-authored content at Lauds -- reused
under his own id, since resolveOfficeBundle resolves a whole bundle per
id, not per part. Vespers content inferred by the same pattern (his real
Vespers is permanently masked by the Octave of St. John the Baptist's
Nativity closing the same day every year checked, 2026-2030 -- not
modeled in this app), not independently verified.

Updates one existing test whose date (2026-06-30) now correctly shows
his own proper chapter winning over the plain Common fallback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 10:08:25 -04:00
will 3ca59bb370 Import St. Benedict's own proper Lauds/Vespers content
Deploy / deploy (push) Successful in 1m19s
Live-verified against Divinum Officium (Monastic Tridentinum 1617),
2033-03-21 (clean year), cross-checked directly against the source's own
Monastic-specific file (SanctiM/03-21.txt), which carries a full proper
office for his Transitus despite the plain secular Sancti file having
none.

Own chapter (Eccli 50:6-7, "Ecce Confessor magnus") and own hymns at
both hours ("Inter ætérnas súperum corónas" Lauds, "Láudibus cives
résonent canóris" Vespers, recounting Subiaco and the founding of Monte
Cassino). Lauds responsory/versicle and Vespers versicle turn out
byte-identical to Common-of-a-Confessor-Bishop's own -- authored
explicitly under his own id rather than left to fall through, since his
actual common (common-of-an-abbot) resolves to a different Confessor-
Not-Bishop responsory that would be wrong here. Vespers has its own
proper responsory invoking him by name ("Sancte Pater Benedícte").
Neither hymn (nor the chapter) has an English rendering in the reference
engine -- own translations, marked draft.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 10:04:56 -04:00
will a168175a7f Import St. Gregory the Great's own proper Lauds/Vespers content
Deploy / deploy (push) Successful in 1m21s
Live-verified against Divinum Officium (Monastic Tridentinum 1617),
2033-03-12 (clean year, per his own saint file's comment). Own chapter
(Eccli 50:1, "Ecce sacérdos magnus... suffúlsit domum") and own hymns at
both hours ("Mella cor obdulcántia" Lauds, "Anglórum jam Apóstolus"
Vespers, the latter recounting his sending Augustine of Canterbury and
the "non Angli sed Angeli" legend); responsory and versicle turn out
byte-identical to Common-of-a-Confessor-Bishop's own, reused verbatim.
Neither hymn has an English rendering in the reference engine at all --
own original translations, marked draft.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 10:00:13 -04:00
will dde539d2e4 Fix Vespers commemoration: a Simplex successor is commemorated too
Deploy / deploy (push) Successful in 1m18s
User-reported: 2026-08-24 Vespers (St. Bartholomew, Duplex II. classis,
keeps his own evening) should have commemorated 2026-08-25's St. Louis
(Simplex), but didn't. This was the exact untested asymmetry the old
commemorationOf's own doc comment flagged as "inferred by symmetry, not
independently live-verified." Live-verifying now shows the two
directions genuinely differ: a Simplex predecessor losing to tomorrow's
First Vespers is correctly excluded (Aug 7 Donatus case, "nihil de
præcedenti"), but a Simplex successor displaced because today keeps its
own Vespers IS still commemorated (Aug 24->25, "commemoratio de
sequenti").

Split the one shared commemorationOf into
commemorationOfDisplacedPredecessor (keeps the Simplex exclusion) and
commemorationOfDisplacedSuccessor (drops it).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 08:57:03 -04:00
will b7036edb35 Confirm Vigils are ferial-by-design; author Common-of-the-BVM Lauds/Vespers bundle
Deploy / deploy (push) Successful in 1m19s
Finished the "ferial/other" audit bucket via each saint's own source
file. Vigils (9 saints) are not a gap at all: every vigil's own [Rule]
explicitly says "Responsory Feria"/"Versum Feria" -- deliberate rubric,
matching this app's existing isFerialOrVigil convention. Even the "ex
C1v" tag on the 6 apostle vigils resolves to the same plain ferial text
when live-queried, confirming no override applies to the Lauds/Vespers
bundle for any vigil.

Common-of-the-BVM (5 feasts) is a real category: each source file says
plain "ex C11"/"vide C11". Live-verified votive=C11: chapter Sir 24:14,
Lauds hymn "O gloriósa Dómina" (the same pre-Urban-VIII text as the
Annunciation's own separate proper), Vespers hymn "Ave maris stella,"
same responsory at both hours, distinct versicles per hour.

Surfaced one masked collision: the Annunciation now resolves through
this Common instead of the ferial default (real improvement, still
interim until its own proper content is imported). Moved the
Passiontide-seasonal-office Vespers test off its date (Mar 25) onto the
clean Mar 30.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 08:52:41 -04:00
will 476f068fde Author Common-of-Several-Confessors Lauds/Vespers bundle (= Confessor-Not-Bishop content)
Deploy / deploy (push) Successful in 1m19s
Read straight from the source (Sancti/02-12.txt, Ss. Seven Holy Founders
of the Servite Order -- the only saint in this category): its own [Rule]
says plain "vide C5," no votive number of its own. Duplicated the same 7
files under common-of-several-confessors.

This closes every Common category this session's audit found with real
users -- resolveOfficeBundle now has real Lauds/Vespers content for
every category, not just the two it started with.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 08:44:05 -04:00
will 98b5c05a84 Author Guardian Angels/Archangels' Lauds/Vespers bundle from St. Michael's Apparition
Deploy / deploy (push) Successful in 1m19s
Methodology upgrade this session: read each saint's own Sancti/MM-DD.txt
[Rank]/[Rule] field directly instead of hunting for a per-saint clean
year to live-query -- it states the real Common assignment (or a direct
proper-content inheritance) with zero calendar dependency.

Guardian Angels/St. Gabriel/St. Raphael the Archangels' own files say
"vide C4," but that's a Missal/Mass-propers designation -- their real
Monastic-breviary file (SanctiM/10-02.txt) cross-references St. Michael's
Apparition (Sancti/05-08, already fully authored in this app) directly
for the chapter/responsory at every hour. No common-of-an-angel category
exists in the source at all. Duplicated St. Michael's Apparition's 7
Lauds/Vespers files under each of the three saints' own ids (proper
content, not a Common fallback), live-verified on their own real 2026
days.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 08:41:46 -04:00
will 0e1a1fe08e Author Common-of-a-Holy-Woman and Common-of-a-Widow Lauds/Vespers bundles
Deploy / deploy (push) Successful in 1m19s
Both genuinely distinct from Virgin(-Martyr) -- different chapter
Prov 31:10-11 "Mulíerem fortem," different hymn "Fortem viríli péctore"
-- but share that category's exact responsory pattern and versicle
byte-for-byte in both hours. Live-verified via St. Anne's own real day
(Jul 26). St. Monica's real day (May 4) confirms common-of-a-widow
shares the identical content with common-of-a-holy-woman, so the same 7
files were duplicated under that id too -- first content authored for
either category at all.

Proof: St. Anne's and St. Monica's own days for Lauds, St. Margaret of
Scotland's real First-Vespers anticipation (Jun 9 -> 10) for Vespers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 08:24:01 -04:00
will 751594d35d Author Common-of-a-Confessor-Doctor and Common-of-an-Apostle Lauds/Vespers bundles
Deploy / deploy (push) Successful in 1m20s
common-of-a-confessor-doctor (4 saints) is another "same content,
different id" case: live-verified votive=C4a byte-identical to plain
votive=C4 (Common-of-a-Confessor-Bishop), so duplicated verbatim rather
than authored fresh, matching the already-existing matins-hymn-common-
of-a-confessor-doctor.yml precedent.

common-of-an-apostle (3 saints/feasts) is genuinely distinct: chapter
Eph 2:19-20, hymn "Exsúltet cælum láudibus" (byte-identical Lauds/
Vespers), shared versicle "Annuntiavérunt ópera Dei." Also duplicated
under the separate common-of-apostles (plural) id used by the Octave of
Ss. Peter and Paul.

Proofs: St. John Damascene (own day) + St. Robert Bellarmine (First
Vespers) for Confessor-Doctor; Commemoration of St. Paul (own day) + the
Beheading of St. John the Baptist (First Vespers) for Apostle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 08:19:23 -04:00
will 7f9a1f7c98 Author Common-of-a-Virgin-Martyr / Common-of-a-Virgin Lauds/Vespers bundle
Deploy / deploy (push) Successful in 1m19s
Live-verified against Divinum Officium (Monastic Tridentinum 1617)
votive=C6: chapter 2 Cor 10:17-18, hymn "Jesu, coróna Vírginum" (byte-
identical Lauds/Vespers), shared versicle "Diffúsa est grátia in lábiis
tuis" -- only the two responsories differ between the hours. Cross-
checked plain common-of-a-virgin (St. Praxedes) renders the same real
content, matching the already-existing benedictus-antiphon-common-of-a-
virgin.yml precedent -- duplicated the same 7 files under that id too.

Proof: St. Apollonia's own clean year (2029-02-09, her 2026 date collides
with St. John of Matha) for Lauds, St. Praxedes's real First-Vespers
anticipation for Vespers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 08:14:05 -04:00
will cf34a6f945 Author common-of-an-abbot Lauds/Vespers bundle (= Confessor-Not-Bishop content)
Deploy / deploy (push) Successful in 1m18s
Same finding as common-of-a-confessor: live-verified (St. Maurus, Jan 15;
St. Odo, Feb 7) that Common-of-an-Abbot's real Lauds/Vespers content is
identical to Common-of-a-Confessor-Not-Bishop's -- an Abbot isn't
necessarily a bishop, so the traditional rite reuses the plain Confessor
common for them. Duplicated the same 7 files under this id, matching the
existing benedictus-antiphon-common-of-an-abbot.yml precedent. Proof
tests on St. Maurus (Duplex II. classis, own Lauds + real First-Vespers
anticipation Jan 14 -> 15).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 08:08:24 -04:00
will 60f3bcc4db Author common-of-a-confessor Lauds/Vespers bundle (= Confessor-Not-Bishop content)
Deploy / deploy (push) Successful in 1m20s
Fixed a real audit-methodology bug first: the bulk gap-audit was sampling
each saint's own date via prayVespera, which reflects whatever saint
claims First Vespers that evening (often a neighboring day's), not the
sampled saint's own category. Redid it via prayLaudes (no anticipation).

That surfaced the real finding: common-of-a-confessor (37 saints, this
app's largest remaining gap) isn't a distinct liturgical Common -- it's
common-of-a-confessor-not-bishop under a different label. St. Joseph's
and St. Thomas Aquinas's chapter is byte-identical to the already-
authored Sir 31:8-9 text, and several of the 37 saints' own file comments
already say "vide C5" (Confessor Not a Bishop) directly. Not relabeled
across the 37 saint files, since matins-hymn-common-of-a-confessor.yml
and benedictus-antiphon-common-of-a-confessor.yml already establish this
plain id as the real, working one for those saints' Matins/Benedictus
content -- duplicated the Lauds/Vespers bundle under this id too, to
match that existing convention.

Updates two existing tests whose dates collided with this fix: the
FERIAL_MONDAY constant (moved off St. Raymond Nonnatus's own day) and a
new Vespers proof on St. Joseph's First Vespers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 08:05:31 -04:00
will 716937b8f9 Author Common-of-a-Confessor-Bishop Lauds/Vespers chapter/responsory/hymn/versicle
Deploy / deploy (push) Successful in 1m22s
Third Common category (17 real users) with no Lauds/Vespers bundle
authored. Live-verified against Divinum Officium (Monastic Tridentinum
1617) votive=C4: chapter Sir 44:16-17 "Ecce sacérdos magnus," Lauds hymn
"Jesu Redémptor ómnium," shared versicle "Justum dedúxit Dóminus per vias
rectas." Found the Vespers hymn is byte-identical to Common-of-a-
Confessor-Not-Bishop's own ("Iste Conféssor Dómini sacrátus") even though
the chapter, both responsories, and the Lauds hymn all genuinely differ
between the two categories -- reused that already-authored text verbatim
rather than re-deriving it.

No real Duplex-II-classis+ saint exists in this category, so the Vespers
proof uses St. Ubald's real First-Vespers anticipation instead of a
votive-only proof. Updates one existing test (2026-04-20, Paschaltide
seasonal office) whose date turned out to also anticipate St. Anselm's
First Vespers, previously masked by this same gap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 07:50:38 -04:00
will 52d6810f3a Author Common-of-Several-Martyrs Lauds/Vespers chapter/responsory/hymn/versicle
Deploy / deploy (push) Successful in 1m24s
Next-largest remaining Common category (22 real users) with no Lauds/
Vespers bundle authored, same fix pattern as Confessor-Not-Bishop and
Martyr(-Bishop). Live-verified against Divinum Officium (Monastic
Tridentinum 1617) votive=C3: chapter Sap 3:1-3, Lauds hymn "Rex glorióse
Mártyrum," Vespers hymn "Sanctórum méritis ínclyta gáudia," shared
versicle "Exsultábunt Sancti in glória." Cross-checked votive=C3/C3a/C3b
byte-identical, confirming the Missal-collect split (plain-martyrs vs.
Pope-martyrs) doesn't carry through to this Office bundle. Proof tested
on Ss. Placid and Companions (Duplex II. classis, real un-outranked
Second Vespers).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 07:45:12 -04:00
will 7816450086 Author Common-of-a-Martyr(-Bishop) Lauds/Vespers chapter/responsory/hymn/versicle
Deploy / deploy (push) Successful in 1m18s
St. Zephyrinus (Aug 26, common-of-a-martyr-bishop, Simplex) claims First
Vespers over St. Louis's Aug 25, but that evening was showing the plain
ferial hymn instead of a real Common-of-a-Martyr hymn: no content had
been authored yet for this Common category's Lauds/Vespers bundle, so
resolveOfficeBundle's chapter lookup came back missing and the whole
bundle fell through to the ferial default.

Live-verified against Divinum Officium (Monastic Tridentinum 1617)
votive=C2b: chapter Jac 1:12, Lauds hymn "Martyr Dei, qui unicum," Vespers
hymn "Deus tuorum militum," shared versicle "Justus ut palma florebit."
Used the votive-office view rather than a real saint's own day for the
same structural reason as the earlier Confessor-Not-Bishop fix: every
real Common-of-a-Martyr-Bishop saint either has proper content of his own
(St. Lawrence, St. Stephen) or never reaches the rank needed to keep his
own Vespers (St. Zephyrinus, St. Callistus). Cross-checked votive=C2/C2a/
C2b/C2-1 all render identical text, so the same 7 files were duplicated
under the plain common-of-a-martyr id too (St. Hadrian, St. Hermes, St.
Fidelis of Sigmaringen).

Updates one existing Vespers test whose date (2026-03-03) turned out to
also anticipate St. Lucius I's First Vespers, previously masked by this
same content gap; adds two new proof tests and a TODO.md entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 07:19:02 -04:00
will 63fd319509 Fix Vespers/Compline header to use evening day, not daytime day
Deploy / deploy (push) Successful in 1m19s
day-nav.ts's header always resolved via resolveDay(date) regardless of
selected hour, so viewing Vespers/Compline on a day whose evening
anticipates tomorrow's First Vespers (e.g. Aug 25, 2026: St. Louis's day,
but St. Zephyrinus claims First Vespers per calendar/vespers.ts) still
showed today's saint in the header while the hour's actual content had
already switched to tomorrow's. Now uses resolveEveningDay for those two
hours specifically.

Also adds an explicit "First Vespers (of tomorrow)" / "Second Vespers (of
today)" status line to the Vespers header, via new
getVespersStatusLabel(), so the anticipation/commemoration result is
visible rather than only inferable from which saint's name shows up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 07:02:21 -04:00
will d94b871bf8 Make day-nav/language-toggle real links, group hours into rows
Deploy / deploy (push) Successful in 1m19s
Day navigation (prev/today/next) and the language toggle were plain
buttons wired to click handlers only; switched them to real <a href>
elements (built via router's new exported urlFor) so right-click,
middle-click, and open-in-new-tab work, with a click handler that
still SPA-routes on a plain left-click and lets modified clicks fall
through to normal browser navigation.

Language selection is now reflected in the URL as a ?lang= query
param (parsed/pushed by router.ts, omitted for the default single-
English case to keep that URL clean), so a shared link preserves the
viewer's chosen language(s).

hour-list.ts now groups the 8 hours into three traditional rows
(night office, day hours, evening) instead of one flat wrapped list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 06:47:15 -04:00
will 7fb37f6237 Fix First Vespers rank threshold: any sanctoral winner qualifies
Deploy / deploy (push) Successful in 1m22s
hasFirstVespers() required Semiduplex-or-higher, so a bare Simplex
feast could never anticipate the evening before it. Live-verified
this was too strict: St. Zephyrinus (Aug 26, plain Simplex) claims
First Vespers over St. Louis (Aug 25, also Simplex) in the reference
engine -- "Vespera de sequenti; nihil de præcedenti." A Simplex feast
has no Second Vespers of its own, but it does still get First Vespers;
there's no rank floor at all, only keepsOwnSecondVespers' separate
question of whether the preceding day outranks the claim.

This also surfaces St. Zephyrinus's Magnificat antiphon, which now
actually gets used (St. Louis, previously shown by the old bug, is
permanently boxed out of ever governing real Vespers in this merged
calendar -- Bartholomew outranks him the evening before, Zephyrinus
the evening after, every year -- so his own Magnificat antiphon was
never authored). Added st-zephyrinus-magnificat-antiphon.yml, live-
verified bilingual text ("Iste Sanctus...").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 06:45:31 -04:00
will 5298cbe4a3 Add suffrages to Vespers, shared with Lauds' existing mechanism
Vespers had no suffrages (Holy Cross, BVM, Joseph, Apostles, Peace)
at all -- types.ts even documented it as Lauds-only. Live-verified
against the reference engine (Monastic Tridentinum 1617): the
"Suffragium" block after the day's collect(s) renders byte-identical
after both Lauds and Vespers, so this was an honest content gap, not
a deliberate omission.

Extracted the suffrage ids/labels and full omission logic (Double-or-
higher, Christ the King, Advent/Christmastide/Passiontide, active
octaves, Cross/Marian-Saturday special cases) out of lauds.ts into a
shared resolveSuffrages() in resolve-common.ts, and wired it into
vespers.ts/vespers.yml the same way Lauds already uses it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
2026-08-26 06:45:20 -04:00
will 13ffce00bf Add Preces (Kyrie/Pater Noster + short litany) to Terce, Sext, and None
Deploy / deploy (push) Successful in 1m19s
Real Monastic 1617 has no Preces at the Little Hours, and that was
previously documented as intentional. Per this project's "not a
reconstruction" design, Prime was recently given Preces in the secular
Tridentine form as a deliberate deviation from Monastic — do the same
here: the reference engine's ferial minor-hour Preces (Kyrie, Pater
Noster, then "Domine Deus virtutum, converte nos..." V/R pairs) is
authored as a single shared common id and wired into all three hours
after the chapter. By explicit choice this is said every day regardless
of rank, mirroring Prime's own "every ferial day" simplification, rather
than importing the reference engine's Sunday/feast form, whose sourcing
(a V/R block reused from Compline) looks like a data quirk rather than
genuine minor-hour content.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXySSbbUh1EhdN1RnAoCaP
2026-08-25 12:30:16 -04:00
will f08507adfc Reformat Terce/Sext/None capitulum text as multi-line block scalars
Every capitulum-*.yml under the three Little Hours combined the chapter,
"R. Deo grátias"/"Thanks be to God", and the trailing V./R. pair into one
flat single-line string. The .lang-column CSS already renders on
white-space: pre-line (as little-hour-conclusio.yml's block-scalar text
already demonstrated), so the run-together rendering was purely a missing
line-break bug in the source YAML, not a CSS or resolver issue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXySSbbUh1EhdN1RnAoCaP
2026-08-25 12:30:04 -04:00
will 851ac90638 Update ferial-Preces TODO note to mention the Vigil-day change
Deploy / deploy (push) Successful in 1m17s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWN869GrMFHqrer9fdCBbF
2026-08-25 08:07:00 -04:00
will fc5c9297fa Give Prime's Preces the ferial form on Vigil days
isSundayOrFeastOffice treats any sanctoral winner, Vigils included, as a
"feast," so a Vigil day was getting the Sunday/feast Preces. Add a
by-day-kind 'vigilIsFerial' flag, set only on Prime's Preces part (its
capitulum keeps the old behavior), so a Vigil-ranked winner now routes
to the ferial Preces text instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWN869GrMFHqrer9fdCBbF
2026-08-25 08:07:00 -04:00
will 33c3890ccf Note Prime's generous ferial-Preces gating as a future-fix TODO
Deploy / deploy (push) Successful in 1m16s
Record that showing the ferial Preces on every ferial day (rather than
only Advent/Lent/vigils/Ember, per the historical rubric) is a
deliberate, currently-unaddressed backlog item, not an oversight.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWN869GrMFHqrer9fdCBbF
2026-08-25 07:55:45 -04:00
will 472878da47 Switch Prime's Sunday/feast Preces to the secular Tridentine form
Monastic Prime's own Dominicales Preces drop the "Et ego ad te, Domine,
clamavi..." versicle block and shorten the closing "Miserere nostri,
Domine... Fiat misericordia tua..." set. Prefer the secular Tridentine
form instead (live-verified against Divinum Officium's "Tridentine -
1906" version), keeping "beato Patri nostro Benedicto" in the Confiteor
by explicit choice even though the real secular Confiteor omits it.

The Sunday/ferial by-day-kind split stays as-is; the ferial form
(prime-preces-feriales.yml) is unchanged and continues to show on every
ferial day by deliberate choice, more generously than the historical
rubric.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWN869GrMFHqrer9fdCBbF
2026-08-25 07:55:41 -04:00
will be45f9609e Add Lauds/Vespers Common bundle gap to TODO's top-of-file summary
Deploy / deploy (push) Successful in 1m17s
Was already recorded in the dated 2026-08-25 entry but missing from
the priority-ordered summary at the top of the file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWN869GrMFHqrer9fdCBbF
2026-08-25 07:32:55 -04:00
will 750ccbf6d4 Author Common-of-a-Confessor-Not-Bishop Lauds/Vespers content
Deploy / deploy (push) Successful in 1m16s
Proof content for the new Common-fallback mechanism, live-verified
against Divinum Officium (Monastic Tridentinum 1617): Lauds chapter/
responsory/hymn/versicle ("Jesu coróna célsior"), cross-checked
byte-identical between St. Louis's and St. Nicholas of Tolentino's own
days. Vespers bundle (hymn "Iste Conféssor Dómini sacrátus", reusing
the already-authored Matins hymn translation) sourced via the
reference engine's votive=C5 view instead of a real saint's own Second
Vespers, since every saint in this Common category has their own
Second Vespers permanently outranked by an adjacent fixed feast.

TODO.md records the fuller sourcing detail and the remaining open
Common categories.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWN869GrMFHqrer9fdCBbF
2026-08-25 07:31:02 -04:00
will 9571a807de Lauds/Vespers chapter-hymn bundle: fall back to Common at any rank
resolveOffice in both hours only substituted a feast's own proper
chapter/responsory/hymn/versicle, gated at duplex-majus+ (piggybacking
eligibility on Lauds' psalmody-override table). A simplex/semiduplex
feast with no proper content of its own dropped straight to the plain
ferial default, skipping their Common entirely (caught via St. Louis,
Simplex, common-of-a-confessor-not-bishop).

Add resolve-common.ts's resolveOfficeBundle: tries the winner's own
proper bundle first, then their Common's bundle, eligibility tested
rank-agnostically via the existing getMinorHourOverrideId (any
sanctoral winner) rather than duplex-majus+ — matching the rule
already used for the minor hours. Applied to both Lauds and Vespers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWN869GrMFHqrer9fdCBbF
2026-08-25 07:30:50 -04:00
will 2d957ae111 Render Lauds/Matins canticle titles bilingually
Deploy / deploy (push) Successful in 1m20s
English used the same id-derived Latin title (e.g. "Canticum Ezechiae")
for every language. Now English uses the authored name from
lauds-canticles.ts when one exists, falling back to the derived title
for names already conventional untranslated in English (Benedictus,
Magnificat, Nunc Dimittis) and Matins' scripture-ref-derived ids.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWN869GrMFHqrer9fdCBbF
2026-08-25 06:55:34 -04:00
will d3ebd90967 Fix duplicate flex mark in Lauds Psalm 148 v.1
Deploy / deploy (push) Successful in 1m20s
The English text had a static ‡ baked in, colliding with the
dynamically-computed flex mark on Tuesday/Thursday Lauds when the
Laudate antiphon's incipit matches the first four words of verse 1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWN869GrMFHqrer9fdCBbF
2026-08-25 06:49:44 -04:00
will c3e87ac57a Record Common-of-Several-Martyrs Matins psalmody work in TODO.md
Deploy / deploy (push) Successful in 1m24s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpKT73TSqRNnE2CSD8GpUY
2026-08-25 06:41:21 -04:00
will e82f3ac018 Author Common-of-Several-Martyrs Matins psalmody, largest remaining category
Live-verified against Ss. Fabian and Sebastian's clean date (2029-01-20,
Monastic Tridentinum 1617). Genuinely distinct psalm scheme from
common-of-a-martyr (single martyr), not a duplicate. Adds Wisdom 3:1-6
to the scripture store for Nocturn 3's canticles. Swaps the matins.test.ts
ferial-fallback proof to St. Anthony Abbot now that Fabian/Sebastian
resolve through this new category instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpKT73TSqRNnE2CSD8GpUY
2026-08-25 06:41:18 -04:00
will 3eda85c684 Add unconditional short litany before Matins collect
Deploy / deploy (push) Successful in 1m21s
Reuses the existing lauds-short-litany proper (Kyrie eleison x3 + Pater
noster) rather than a Matins-named duplicate, same reuse discipline
Vespers already applies. Unlike Lauds/Vespers's own preces, this fires
every day with no ferial/Sunday swap and is never omitted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpKT73TSqRNnE2CSD8GpUY
2026-08-25 06:32:33 -04:00
will c707bb1b75 Move John 17 bible-plan reading from Sunday to Saturday after Ascension
Deploy / deploy (push) Failing after 1m4s
Combines it with the existing John 21 reading on Saturday. Also brings
the plan back in line with matins.ts's own documented invariant that
the user's reading plan never assigns a Gospel reading on a Sunday.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jHSimu99UDKh1pRRd11uV
2026-08-25 06:24:59 -04:00
will 3cf1a7d6d6 Record Common-of-a-Confessor Matins hymn work in TODO.md
Deploy / deploy (push) Successful in 1m27s
Marks the Confessors (C4) hymn gap from the 2026-08-24 entry as done
and logs the Monastic-vs-secular textual-variant finding as a general
lesson about not trusting reused reference-engine translations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jHSimu99UDKh1pRRd11uV
2026-08-25 06:17:40 -04:00
will 9e0bdb3484 Author Common-of-a-Confessor Matins hymn ("Iste Confessor")
Fixes the ferial-hymn fallback for St. Louis and 82 other confessor
saints across all 5 confessor common ids, using the pre-Urban VIII
(Monastic-track "sacrátus") textual variant with a fresh English
translation rather than the reference engine's mismatched reused one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jHSimu99UDKh1pRRd11uV
2026-08-25 06:17:36 -04:00
will 8bcecd108b Record Matins reading/hymn/invitatory fixes and hour dedup in TODO.md
Deploy / deploy (push) Successful in 1m24s
Documents the full set of changes from this session: the
nocturnReadingIds precedence bug fix, the new Matins hymn Common-tier
and invitatory-antiphon mechanism (Common-of-an-Apostle content
authored, other Commons/seasons logged as backlog), the antiphon-
doubling dedup, and the Terce/Sext/None hymn dedup — including why the
Lauds/Vespers office-bundle resolvers were deliberately left alone
(they turned out not to be byte-identical: different override-
eligibility semantics) and why Matins's own octave hymn tier isn't
redundant with resolveOfficeWinner's octave-folding.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F25189JqjXddUhU9hM9nUS
2026-08-24 07:14:27 -04:00
will 72033221c3 Collapse Terce/Sext/None's identical hymn resolution into one helper
Terce, Sext, and None each had a byte-identical 'hymn' case
(resolveCommon(part.textRef.id), no doxology mechanism, no override/
season tiering). Replace all three with a single shared
resolveSelfContainedHymn helper in resolve-common.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F25189JqjXddUhU9hM9nUS
2026-08-24 07:14:19 -04:00
will 449472d5aa Add Matins hymn Common-of-Saints tier and a real invitatory-antiphon mechanism
resolveMatinsHymn gained a Common-category tier (override -> octave ->
Common category -> season -> ferial), reusing matins-psalmody-
overrides.ts's own getSaintRecord(id)?.common lookup pattern. The
invitatory antiphon (invitatoryParts), previously one hardcoded fixed
text for every day of the year with only full-vs-incipit doubling
varying by rank, now goes through a real resolveMatinsInvitatoryText
(override -> Common category -> season -> ferial, no octave tier) before
the doubling step.

Content authored this pass: Common-of-an-Apostle only, both hymn
(matins-hymn-common-of-an-apostle.yml, "Aeterna Christi munera") and
invitatory antiphon (matins-invitatory-common-of-an-apostle.yml, "Regem
Apostolorum Dominum"), both read directly from the reference engine's
Commune/C1.txt. Live-verified: St. Bartholomew (2026-08-24) and St.
Andrew (2026-11-30) both now resolve to this Common's text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F25189JqjXddUhU9hM9nUS
2026-08-24 07:14:06 -04:00
will 8f55ec6931 Centralize antiphon opening-doubling logic across all hours
The same idiom (splitNamedAntiphon + isDoubleOrHigher(...) ? full :
incipit) was copy-pasted at 11 call sites across 8 hour files. Add one
shared openingAntiphon(antiphon, winner) helper in resolve-common.ts
and use it everywhere a psalm/canticle antiphon's opening form is
decided. Pure dedup, no behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F25189JqjXddUhU9hM9nUS
2026-08-24 07:13:38 -04:00
will f8de318b7f Fix Matins nocturn readings leaking suppressed Sunday content
nocturnReadingIds unconditionally pooled the plain temporal-cycle and
month-week reading ids into every day's nocturn-reading pool, even when
a real feast wins the day outright with zero commemorations
(decideOccurrence's `ordinary-feria` branch, which correctly suppresses
the temporal identity entirely). St. Bartholomew (duplex-2-classis,
2026-08-24, a Monday) has no Nocturn 3 content of his own, so the
leftover 13th-Sunday-after-Pentecost and month-week readings wrongly
filled his Nocturn 3.

Gate the pooling on whether the day's own occurrence decision actually
retained the temporal identity (day.winner.kind === 'temporal', or a
`temporal`-kind entry in day.commemorations) — mirrors getDayCollects's
existing pattern for the same question.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F25189JqjXddUhU9hM9nUS
2026-08-24 07:11:09 -04:00
will 42dd4dc04b Add Te decet laus to Matins, drop Ps 129/50 from ferial Preces
Deploy / deploy (push) Successful in 1m24s
Matins now sings "Te decet laus" (Psalterium/Common/Prayers.txt's [Te
decet]) immediately after the Te Deum on every 3-nocturn day, matching
Rule of Benedict ch. 11's close of Vigils. New common text
te-decet-laus.yml, wired into both matins.ts branches that render a Te
Deum; absent on ferial days along with it.

Also drops Psalm 129 (De profundis) from the Lauds ferial Preces and
Psalm 50 (Miserere) from the Vespers ferial Preces, per direct
instruction — both were embedded inline in the live-transcribed text.
Updated the affected fixture tests to key off a versicle that survives
the removal instead of the now-absent psalm text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWzE3kaSZCs79fPBrrE1aF
2026-08-23 09:03:04 -04:00
will 0fb3b4c13e Prepend R. to nocturn-readings responsories missing their opening marker
Deploy / deploy (push) Successful in 1m26s
All 53 nocturn-readings files with a responsory field started straight
into the text with no leading R., unlike octave-readings' own R./V.
convention. Fixed in content per direct instruction (not injected at
render time), matching each file's own existing plain V./R. style rather
than switching to octave-readings' R./V. glyphs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWzE3kaSZCs79fPBrrE1aF
2026-08-23 08:51:16 -04:00
will ca28f2ef5f Fix Matins reading-pool ordering when Nocturn 2/3 come from different sources
buildReadingPool concatenated readings in id-processing order, so a later
post-Pentecost Sunday's Nocturn 3 (from its own temporalId file) could land
ahead of its Nocturn 2 (from the newly-added month-week id) in the pool,
flipping which nocturn slot each landed in. Group by each reading's own
nocturn tag instead, ascending, preserving id/file order within each group.

Also logs three follow-up items to TODO.md: displaying "Te decet laus",
the missing responsory marker convention in nocturn-readings content, and
removing Ps 129/50 from the Lauds/Vespers ferial Preces.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWzE3kaSZCs79fPBrrE1aF
2026-08-23 08:02:16 -04:00
will 72a0f8072e Import calendar-month/week Matins Nocturn 2 content for Aug-Nov
Deploy / deploy (push) Successful in 1m24s
The later post-Pentecost Sundays' real Nocturn 2 patristic content isn't
keyed by Pentecost offset in the reference engine — it's keyed by civil
calendar month/week (the old "Scriptura occurrens" cycle), which shifts
relative to post-pentecost-NN every year depending on Easter's date. Port
Date.pm's monthday() algorithm (pre-1955/Tridentine rubrics) as a new,
date-driven id, author all 20 August-November month-week files from the
reference engine's Tempora/0MN-0.txt sources, and pool them in
hours/matins.ts as a third, independent reading source alongside the
existing temporalId lookup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWzE3kaSZCs79fPBrrE1aF
2026-08-23 07:45:37 -04:00
will 25e2eeb493 Author Matins Nocturn 2/3 patristic readings for the whole post-Pentecost season
Deploy / deploy (push) Successful in 1m27s
Closes the gap the user found on 2026-08-23 (post-pentecost-13's Matins had
no patristic reading): nocturn-readings/ had content for only one temporal
id (post-pentecost-15, the original mechanism proof date). Since
resolveTemporalId maps every day in a liturgical week to the same id, one
file per id covers the whole week — authored all 23 remaining
post-pentecost-NN ids (01-14, 16-24) from the reference engine's own
Tempora/PentNN-0.txt files. Trinity Sunday (01) keeps its own full proper
office including a displaced-Sunday commemoration; 02-11 have real Nocturn
2+3 content; 12 onward are Nocturn-3-only (homily, Gospel incipit skipped
per convention), reusing the same two responsories the source itself
cross-references week to week.

Rest of the temporal cycle (Advent/Christmas/Epiphany/Septuagesima/Lent/
Easter) is a separate follow-up pass, tracked in TODO.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWzE3kaSZCs79fPBrrE1aF
2026-08-23 07:10:20 -04:00
will 7858692fa7 Wire the flexa mark into every hour's psalm-antiphon pairing
Deploy / deploy (push) Successful in 1m24s
Every psalm's opening antiphon (Lauds, Prime, Terce, Sext, None,
Vespers, Compline's callers, Matins) now runs through applyFlexaMark,
marking the boundary in both the displayed antiphon and the psalm's
first verse — e.g. today's Sunday Matins Nocturn 1: "Ant. The king
rejoices ‡" / "The king rejoices ‡ in thy strength, O Lord...".
Live-verified against the reference engine's own rendering for Ps 20.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UCvvgFLrhXNVFUThz6kanw
2026-08-23 06:30:58 -04:00
will 72b30fad80 Add flexa-mark (‡) boundary computation for antiphon/psalm text
Ports Divinum Officium's getantcross/depunct (horas.pl): finds where a
displayed antiphon's words stop matching a psalm's first verse, so the
mark can later show where the antiphon leaves off and the psalm's own
text resumes. Not yet wired into any hour builder.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UCvvgFLrhXNVFUThz6kanw
2026-08-23 06:30:23 -04:00
will 9fb9cb5e24 Add Ember-day mechanism (privileged feria, not a rank contest) + real content
Deploy / deploy (push) Successful in 1m17s
Researched whether September/Advent Ember days belong in the
movable-feasts.ts table alongside Christ the King/Immaculate Heart of
Mary. Live-verified against the reference engine they don't: Ember days
are privileged ferias (a saint can still win outright against them), not
named feasts contesting the day by rank.

Two smaller pieces instead: calendar/temporal.ts's septemberEmberDayOffset
(Sunday nearest Sept 14, same "nearest" arithmetic adventStart already
used, factored into a shared nearestSunday helper) feeds a new
resolveTemporalCategory check giving those 3 dates privileged-feria-minor
(live-verified correct); calendar/ember-days.ts's applyEmberDay relabels
the day's own temporal id to the Ember day's own (so its real content is
found) when the feria itself wins, or adds a commemoration when a saint
does. Advent Ember days needed no precedence change -- Advent's own
season default already covers them.

Found and fixed a real bug along the way: matins.ts's nocturnReadingIds
only ever pulled a commemorated *sanctoral* id's own readings, never a
commemorated *temporal* one's -- so a commemorated Ember day (the common
case) would never have surfaced its own content. Same root cause as the
day.winner.id gap the IHM relocation fixed earlier, just hiding in the
commemorations loop instead.

Authored all 6 days' real collect + 3 Matins readings each, transcribed
directly from the reference engine (September: Tempora/093-{3,5,6}.txt;
Advent: Tempora/Adv3-{3,5,6}.txt, whose lessons are themselves a
cross-reference to the Annunciation's own Common, followed and
transcribed from there). Kept as 3 separate readings per day rather than
the usual combine-into-one default, since each carries its own
genuinely distinct proper responsory.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZSAgRi4QE4XRTqVto93zA
2026-08-22 07:27:52 -04:00
will 879d4d607c Generalize movable-feast anchors beyond Easter offsets, migrate Christ the King
Deploy / deploy (push) Successful in 1m18s
Extends TemporalFeastRecord's date anchor from a single easterOffset
number into a MovableAnchor union: 'easter' (unchanged), plus
'nth-sunday-of-month' (nth: -1 for "last") and 'nth-sunday-of-advent' --
both new anchor kinds resolve to "Nth Sunday counting from an anchor
date," just with a different anchor (first-of-month vs. Advent's own
start, which is itself always a Sunday). Adds calendar/temporal.ts's
sundayOnOrAfter to support the month case.

Migrates christ-the-king.yml onto the new nth-sunday-of-month anchor
(month: 10, nth: -1) with a new `unconditional: true` flag replacing its
old always-wins-no-rank-check bespoke function -- retiring
applyChristTheKing and its lastSundayOfOctober helper from
calendar/index.ts entirely. Down to two bespoke override functions left
(marian-saturday's date-less fallback, christmas-octave-sunday's
fixed-date-range Sunday search), both structurally different from the
Nth-Sunday-from-anchor shape this table now covers.

No behavior change -- the full existing Christ the King test suite
passes unchanged through the new generic path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZSAgRi4QE4XRTqVto93zA
2026-08-22 07:01:28 -04:00
will 5e79fb4edf Generalize non-Sunday movable feasts into one Easter-offset resolver
Deploy / deploy (push) Successful in 1m21s
The bespoke applyImmaculateHeart function (following the applyMarianSaturday/
applyChristTheKing precedent of one hand-written override function per
feast) doesn't scale: several more feasts are planned that all reduce to
"weekday N days from Easter Sunday" (Ember/Rogation days, more
Sacred-Heart-family Marian devotions, St. Joseph's pre-1955 Eastertide
feast, Lenten Friday Passion devotions), and one-off functions invite
subtle ordering bugs -- applyImmaculateHeart had to be sequenced after
applyMarianSaturday specifically or it would have been silently clobbered.

Replaced with calendar/movable-feasts.ts's generic applyMovableFeasts,
which scans calendar/temporal-feasts.ts's existing per-feast YAML records
for a new optional `easterOffset` field. Adding the next movable feast is
now a new data/calendar/temporal-feasts/<id>.yml file with `rank` and
`easterOffset` set, not a new TypeScript function. Same rank-compared,
commemorate-the-loser semantics as before, just centralized instead of
duplicated per feast.

christ-the-king, marian-saturday, and christmas-octave-sunday stay as
their own functions -- they're structurally different rules (a fixed
month-position search, a date-less fallback default, and a fixed-date-
range Sunday search, respectively), not Easter offsets, so force-fitting
them into this table wouldn't actually simplify anything.

No behavior change -- the existing Immaculate Heart of Mary test suite
passes unchanged through the new generic path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZSAgRi4QE4XRTqVto93zA
2026-08-22 06:33:15 -04:00
will cf4edfae2b Fix Matins hymn to actually keep the Assumption's octave hymn all week
Deploy / deploy (push) Successful in 1m31s
resolveMatinsHymn's own doc comment already claimed the octave "keeps
the feast's own proper hymn all week," but the code never implemented
it -- it only matched when the day's own winner literally was
`assumption` (Aug 15 itself). Every other octave day (16, 17, 19, 20,
21, none of whom has a Matins hymn of their own authored) fell straight
through to the plain ferial hymn instead.

Adds a real octave-fallback tier between the existing per-feast override
and the seasonal tier. A deliberate departure from the reference engine,
which doesn't do this either (Monastic 1617's octave days have no
[Hymnus Matutinum] override at all) -- consistent with how this project
already treats octave readings/commemorations more generously than any
one source track.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZSAgRi4QE4XRTqVto93zA
2026-08-22 06:00:42 -04:00
will 224f34908c Relocate Immaculate Heart of Mary off Aug 22 to Easter+69
Fixed Aug 22 collided outright with the Assumption's own octave-closing
day (both this project's blended calendar tracks are kept deliberately,
so one shouldn't permanently suppress the other). Moved IHM to the
Saturday after the Feast of the Sacred Heart -- its real diocesan date
from 1914 until Pius XII's 1944 fixed-date decree, and also the date the
1969 reform returned to.

Required real new mechanism, not just a data move: a new bespoke
calendar/index.ts override (applyImmaculateHeart, following the existing
applyMarianSaturday/applyChristTheKing precedent), a move from the
sanctoral saints store to the temporal-feasts store (different propers
lookup entirely), and a real bug fix in matins.ts's nocturnReadingIds,
which only ever picked up a *sanctoral* winner's own reading file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZSAgRi4QE4XRTqVto93zA
2026-08-22 05:59:33 -04:00
will 025d0e3a08 Fix Sunday Matins nocturn versicles dropping their R. line
Deploy / deploy (push) Successful in 1m13s
sundayPsalmNocturn/sundayCanticleNocturn only ever rendered a
versicle's V. line, silently missing the responsory for Sunday Matins
and every Duplex+ weekday feast using matins-psalmody-overrides. Both
now share the versicleText helper introduced for the ferial nocturn
antiphons, which combines V./R. into one block.
2026-08-21 09:31:09 -04:00
will 675b644c68 Add antiphons and versicle to plain ferial weekday Matins nocturns
Deploy / deploy (push) Successful in 1m13s
Every non-Sunday, non-Duplex+ Matins nocturn rendered bare psalms with
no antiphon at all. Sourced each psalm's antiphon from Divinum
Officium's Monastic Matins psalter and regrouped to fit
psalter-distribution.yml's own already-established (and non-historical)
weekday psalm redistribution, since the source's own per-weekday
antiphon pairings don't survive that reslice intact.
2026-08-21 09:27:13 -04:00
will 6fedcd7f2b Add English translations for octave-reading source attributions
Deploy / deploy (push) Successful in 1m13s
Every octave-reading yml's source field held only the Latin
attribution copied verbatim from the breviary text; add the matching
English translation now that the field carries both languages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3wxvz3mPXiHxkPB5JpnmD
2026-08-21 08:04:13 -04:00
will b52b202909 Make octave-reading source attribution bilingual
The heading above each Matins octave reading was a bare Latin string
(e.g. "Sermo sancti Bernárdi Abbátis") rendered as-is in both the
Latin and English columns, since the app shows both languages side
by side rather than toggling. Widen OctaveReadingText.source and the
lesson part's label to carry per-language text, and render it as its
own lang-columns row like the body text beneath it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3wxvz3mPXiHxkPB5JpnmD
2026-08-21 08:04:03 -04:00
will a7da8b0211 Add Assumption's proper Matins hymn
Deploy / deploy (push) Successful in 1m13s
"Surge! Jam terris fera bruma cessit" / "Arise! the cold blasts from
earth have receded", read from the reference engine's Sancti/08-15.txt,
used throughout the Assumption's octave via that override mechanism.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3wxvz3mPXiHxkPB5JpnmD
2026-08-21 07:56:11 -04:00
will 62c74539f8 Add Matins hymn override mechanism
Matins always rendered the plain ferial hymn, unlike Lauds/Vespers which
already check a duplex-majus+ override then a seasonal default first.
Surfaced by the Assumption's octave, which keeps the feast's own proper
hymn all week rather than falling back to ferial.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3wxvz3mPXiHxkPB5JpnmD
2026-08-21 07:56:06 -04:00
will 4835b35fc1 Add Assumption octave commemoration content
Deploy / deploy (push) Successful in 1m13s
Assembled from already-verified Assumption pieces (Benedictus
antiphon, Vespers V/R, post-1950-dogma collect) since the octave
commemoration block this app's own generous-commemorations design
calls for has no direct reference-engine source to copy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3wxvz3mPXiHxkPB5JpnmD
2026-08-21 07:38:55 -04:00
will 1ed15f15c0 Fix getDayCollects skipping octave commemorations on outright-win days
The substitutedOctaveId gate only checked temporalCategory, not who
actually won the day, so a saint winning outright within an active
octave (e.g. St. Bernard vs. the Assumption octave, Aug 20) silently
lost that octave's own commemoration block at Lauds/Vespers. Now
mirrors resolveOfficeWinner's real gate (day.winner.kind and
ALWAYS_OVERRIDE_TEMPORAL_IDS).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3wxvz3mPXiHxkPB5JpnmD
2026-08-21 07:38:49 -04:00
will 833983c875 Switch Assumption collect to the post-1950-dogma text
Deploy / deploy (push) Successful in 1m14s
Monastic 1617 itself is inconsistent: the feast/closing octave day
render the old pre-dogma "Famulórum tuórum..." collect, but the
interior octave days (16/18/19/21) render the newer "Omnípotens
sempitérne Deus, qui Immaculátam..." collect from the 1954 Divino
Afflatu revision -- a data cross-reference slip in the reference
engine, not a deliberate rubric. vu already used one collect uniformly
across the whole octave rather than replicating the split; switch that
shared collect to the newer text per user preference.
2026-08-21 07:26:44 -04:00
will 927f031d6b Add full Ant+V/R+collect commemoration for St. Jane Frances de Chantal
Deploy / deploy (push) Successful in 1m15s
Lauds/Vespers were falling back to a bare collect for her commemoration
since only the collect had been authored. Live-verified the antiphon
and versicle against the reference engine (Divino Afflatu - 1954,
1955-08-21, the nearest year her feast actually fell on a Sunday and
was commemorated rather than won).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3wxvz3mPXiHxkPB5JpnmD
2026-08-21 07:11:01 -04:00
will 212067a823 Add date picker and audio interface to TODO
Deploy / deploy (push) Successful in 1m13s
Two new mechanism-gap items: an arbitrary-date calendar picker (day-nav
currently only steps one day at a time) and a "read it to me" audio
interface via the Web Speech API for long texts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3wxvz3mPXiHxkPB5JpnmD
2026-08-21 06:58:42 -04:00
will 945c5ef109 Default the initial hour to local time of day instead of always Prime
Deploy / deploy (push) Successful in 1m18s
Visiting the app with no hour in the URL previously always showed
Prime for today, since selectedHour was hardcoded in AppState. Now it
picks the hour whose band contains the visitor's local clock time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3wxvz3mPXiHxkPB5JpnmD
2026-08-21 06:50:42 -04:00
will 9e6b36bed9 Lower St. Jane Frances de Chantal to semiduplex
Deviates from the reference engine's plain Duplex per the user's own
calendar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3wxvz3mPXiHxkPB5JpnmD
2026-08-21 06:50:36 -04:00
will 784e0fb5ba Order hour nav by the day, not by build/declaration order
Deploy / deploy (push) Successful in 1m9s
HOUR_IDS drove the nav's render order but was declared arbitrarily;
reorder to Matins, Lauds, Prime, Terce, Sext, None, Vespers, Compline
so the nav reads left-to-right the way the office actually unfolds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 09:33:58 -04:00
will 4373b60f07 Lay out hour nav horizontally and use real links
Deploy / deploy (push) Successful in 1m7s
Matches the reference app's row layout instead of a vertical sidebar
list, and switches from buttons to anchors since hours are real
routes — plain click still SPA-navigates, modifier/middle clicks
fall through to native browser behavior (open in new tab, etc.).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 09:31:46 -04:00
will 15929a6e50 Add Matins' missing closing (Benedicamus Domino / Fidelium animae)
Deploy / deploy (push) Successful in 1m7s
Matins is hand-assembled in TS rather than driven by a data/hours/*.yml
parts list, so it never got the closing wired in that every other hour
has. Live-verified against the reference engine's Monastic Tridentinum
1617 Matutinum "Conclusio" block that Matins' closing text is
byte-identical to lauds-conclusio — reusing it the same way
Vespers already does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 09:24:54 -04:00
will 3e1fd3b9c4 Strengthen the Immaculate Conception's and Nativity BVM's octaves too
Deploy / deploy (push) Successful in 1m8s
Same `wins: duplex` override as the Assumption's and Pentecost's
octaves, applied to two more common-class octaves. Accepted side
effect: St. Damasus (Dec 11) and St. Nicholas of Tolentino (Sep 10),
both Semiduplex, no longer tie-and-win an ordinary octave day here --
they're now commemorated instead of winning outright.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 07:40:39 -04:00
will 6f8f6618b9 Strengthen the Assumption's octave against a demoted St. John Eudes
Demote St. John Eudes (Aug 19) from Duplex to Semiduplex -- too minor
a confessor to keep outranking the Assumption's octave. To make that
demotion actually cede the day, raise the Assumption's octave to
`wins: duplex` (same pattern already used by Pentecost's octave), so an
ordinary-day tie no longer automatically favors the occurring saint.

Promote St. Thomas of Canterbury (Dec 29) to Duplex so he keeps
winning against the Christmas octave stack now that ordinary-day ties
are no longer a given.

Also add a closing-day tie-break to octave-vs-octave precedence
(pickWinningOctave): when two active octaves tie in rank, the one on
its own closing day now wins the label contest, ahead of the existing
"more recently started" tie-break. Needed because Assumption's day 3
and St. Lawrence's own closing day (Aug 17) now tie at Duplex, and the
closing day should still govern that date's label.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 07:39:58 -04:00
will 9364185f70 Omit responsories' own closing Gloria Patri throughout Passiontide
A different, wider rule than the psalm/canticle Gloria (Triduum-only):
responsories drop their Gloria for the whole of Passiontide (Passion
Sunday through Holy Saturday), except on a Sancti feast's own day —
live-verified (St. Joseph, Duplex I, 2027-03-19, landing in Passiontide
that year, keeps it; an ordinary ferial day in the same window omits it,
at both Compline and Prime).

Every responsory's stored text differs, Gloria-bearing vs Gloria-free,
by exactly one fixed-wording line, so resolve-common.ts's new
resolveResponsory filters that line out rather than requiring separate
per-season files or a data migration — mechanism first, per the
project's own stated content-authoring order. Wired into the four hours
that have a real responsory (Compline, Prime, Lauds, Vespers); Lauds/
Vespers' existing hand-authored Passiontide-proper responsories were
already Gloria-free and are unaffected.
2026-08-19 06:38:21 -04:00
will e93fc4032a Drop Matins' opening (versicle, Ps 3, Invitatory, hymn) during the Sacred Triduum
Tenebrae's rubric replaces Matins' usual opening with a silently-said
Pater/Ave/Credo, live-verified against both Monastic Tridentinum 1617
and Divino Afflatu 1954 (Holy Thursday: Invitatorium and Hymnus both
"omittitur", no Ps 3 or versicle either). Since this app already never
renders that silent block on ordinary days either (data/hours/prime.yml),
the fix is a pure omission straight into Nocturn 1, using the same
isInTriduum window added for the Gloria Patri work.
2026-08-19 06:18:07 -04:00
will 7ffb9afea2 Extend closing Gloria Patri to canticles, centralize instead of baking it in
Benedictus, Magnificat, and Nunc Dimittis had the Gloria Patri baked
verbatim into their propers YAML with no Triduum suppression; Matins'
Nocturn 3 OT canticles had none at all. Give the 'canticle' ResolvedPart
the same centrally-attached gloriaPatri field psalms already got, strip
the baked-in text, and drop Lauds' now-redundant local append/Triduum
check — live-verified that canticles follow the exact same Triduum-only
omission as psalms (e.g. Benedictus at Lauds of Holy Thursday).
2026-08-19 06:06:07 -04:00
will b230f3f1e8 Add closing Gloria Patri after psalms, omitted during the Sacred Triduum
Psalms across every hour rendered with no closing Gloria Patri at all.
Add it centrally (hours/index.ts) rather than per-hour, using a new
calendar/temporal.ts isInTriduum helper so it's correctly dropped from
Maundy Thursday through Holy Saturday and nowhere else — live-verified
against both of this app's actual source tracks (Divino Afflatu 1954
and Tridentine 1906), not just the modern Rubrics 1960 default. Also
fixes Lauds' existing but Triduum-blind canticle Gloria Patri append.
2026-08-19 06:03:51 -04:00
will a9f673fef9 Label Matins nocturns and the Invitatory in the rendered ordo
Deploy / deploy (push) Successful in 1m17s
Adds a section-heading ResolvedPart kind and emits "Invitatory" and
"Nocturn I/II/III" headings in matins.ts, so the hour's structural
boundaries are visible instead of rendering as one undifferentiated
stream of parts. Ferial (1-nocturn) days get no Nocturn heading, matching
their existing single-unnumbered-nocturn design.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 05:28:44 -04:00
will e7bbc7c9f5 Add bilingual day-label headers with Latin weekday/season/ordinal vocabulary
Builds every day label as a {en, la} pair (weekdays, ranks, season/ordinal
phrasing authored in Latin; proper names without an authored Latin form
fall back to their English string) and merges the date+day-label into one
header in the day-nav bar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 05:27:44 -04:00
will ef71d9c5bc Distribute Matins readings front-light across nocturns, not evenly
Deploy / deploy (push) Successful in 1m7s
Nocturn 1 and 2 each get exactly one reading; Nocturn 3 absorbs the
rest of the pool, however large. A pool of only one reading goes to
Nocturn 3 rather than Nocturn 1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 13:06:57 -04:00
will 01667e7926 Revert ordinary-Sunday Semiduplex to transferring, per direct instruction
Deploy / deploy (push) Successful in 1m9s
Keeps Simplex commemorating in place, but a Semiduplex feast on an
ordinary Sunday goes back to transferring off to the next open day —
this app's own design choice, not a correction against a reference
engine. The transfer mechanism is already generic: this file's `decideOccurrence`
only signals "couldn't hold onto this candidate" for the day itself;
calendar/index.ts's resolveDay is what independently checks each
neighboring day's own native occurrence for an incoming transfer and
claims it — no separate lookback logic needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 11:56:15 -04:00
will 79372fb274 Don't commemorate a Vigil alongside the very feast it anticipates
At Vespers/Compline, a day's own Vigil losing tonight to tomorrow's First
Vespers was showing up as a redundant extra commemoration alongside that
same feast (e.g. Nov 29's Compline showing both "St. Andrew" and "Vigil
of St. Andrew" for Advent I's anticipated evening).

Added an explicit `vigilOf` field to each Vigil's own saint record
(several Vigil ids don't map cleanly to their feast's id by stripping a
`vigil-of-` prefix — `vigil-of-the-assumption` vs `assumption`,
`vigil-of-st-james` vs `st-james-the-greater` — so this needs to be
explicit rather than guessed from the id or date adjacency).
calendar/vespers.ts's evening-anticipation merge now skips adding the
Vigil commemoration when tomorrow's winner or its own commemorations
already include that same feast.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 11:00:09 -04:00
will f43f030a6a Fix ordinary-Sunday precedence and unify getDayLabel's commemoration rendering
Two related fixes surfaced while chasing why St. Gregory Thaumaturgus
appeared to be misdated on Nov 18:

- calendar/commemorations.ts's `ordinary-sunday` case was transferring a
  Semiduplex (or lower) saint off the Sunday instead of commemorating it
  in place — an earlier, unverified guess. Live-verified against both
  Tridentine 1906 and Divino Afflatu 1954 (St. Gregory Thaumaturgus, St.
  Clement, St. Apollinaris, St. Thomas Becket, all real cases): the saint
  stays and is commemorated on the Sunday itself, same as Simplex, never
  pushed to the next open day. This is what was actually moving Gregory
  onto Nov 18 — not a data error. `applyChristmasOctaveSunday`'s own
  Dec 26-29 special case needed a matching adjustment (per its own
  documented intent, the displaced saint there is deliberately *not*
  commemorated in place, since he reappears in full on Dec 30 instead).

- getDayLabel had five branches, each hand-assembling its own
  filter/format logic for which commemorations to show — which is why
  the octave phrasing, the closing-day title, and a missing Sunday
  commemoration turned into three separate bugs earlier instead of one.
  Replaced with a single shared `collectCommemorations` used by every
  branch. This also exposed that the sanctoral-winner branch never
  showed any commemorated saint at all, and every other branch only
  showed the *first* one (`.find()`), silently dropping real collisions
  — both now show every commemorated saint, matching this app's own
  generous-commemoration design.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 10:59:50 -04:00
will 3f07d6e8d3 Fix octave day labels: correct commemoration phrasing, closing-day title, and privileged-season precedence
Deploy / deploy (push) Successful in 1m4s
Three related day-label bugs, all found via Aug 19's mislabeled Assumption
octave commemoration:

- A sanctoral winner that displaces the only active octave now shows the
  octave's real "Nth Day within the Octave of X" phrasing (no rank, since
  it's riding along under the winning feast), not a bare feast name.
- An octave's own closing day now titles itself "Octave of X", matching
  the real DO's "in octava" vs. "infra octavam" distinction, instead of
  "8th Day within the Octave of X".
- A sufficiently-ranked octave can now outrank a privileged temporal
  season (e.g. the Immaculate Conception's octave outright winning several
  of its days against Advent, live-verified against Divino Afflatu 1954),
  via a new shared octaveGoverningPrivilegedDay helper used by both the
  day label and the actual office-content resolver. Deliberately excludes
  Christmastide, whose own stacked octaves are structurally already that
  season's temporal content rather than a foreign add-on.

Updates a Matins test fixture that had unknowingly relied on the
Dec 15 bug (Advent ferial + a lone commemorated saint) and splits it into
a clean ferial case plus a dedicated commemorated-saint case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 08:49:51 -04:00
will 427ba6756a Show rank in the day label for every winner kind, not just sanctoral
Deploy / deploy (push) Successful in 1m3s
Rank display (added in 1fc4bd7) only fired for a plain sanctoral
winner. Every other case -- an active octave day, a named temporal
feast, a season anchor day (Easter, Trinity Sunday, Epiphany), or the
plain ordinal Sunday/feria fallback -- showed no rank at all, which is
what made today's octave day (Aug 18, day 4 of the Assumption's
octave) show no rank. Octave days use the octave's own already-computed
effective rank; anchor days get a fixed Duplex I Class (Ash Wednesday
excepted -- a privileged feria has no duplex-scale rank); the plain
fallback derives a Semiduplex/Feria label from the day's own
temporalCategory, since Sundays/ferias aren't on the duplex scale at
all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 07:41:57 -04:00
will a68ab716b7 Add rank data to named temporal feast records
TemporalFeastRecord had no rank field at all, so Christmas/Pentecost/
Christ the King/Marian Saturday had nothing for the day label to show.
Christmas, Pentecost, and Christ the King are Duplex I Class (the
latter's own file comment already said so); Marian Saturday is a
Simplex-strength votive commemoration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 07:41:50 -04:00
will 25669dfdae Record the Confessor Matins psalmody categories in TODO.md
Deploy / deploy (push) Successful in 1m4s
Documents the Francis of Paola / Basil the Great live-verification,
the identical-apart-from-versicles finding, the Duplex+ saints checked
for proper-antiphon eligibility, and the M1617-kalendar wrinkle found
along the way (a SanctiM file existing doesn't mean the saint is
actually on the Monastic Tridentinum 1617 calendar).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 07:09:55 -04:00
will e9134b2117 Add Matins test proofs for the new Confessor category tiers
Two new Common-tier proofs (Francis of Paola, Basil the Great) confirm
the resolver picks up both new categories with verified antiphon text.
The old ferial-fallback proof (St. John Bosco, Common of a Confessor)
is now superseded since that category is authored -- swapped for Ss.
Fabian and Sebastian (Common of Several Martyrs, still unauthored).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 07:09:48 -04:00
will b944f18433 Author Common-of-a-Confessor(-Bishop) Matins psalmody overrides
Live-verified against Divinum Officium's Monastic Tridentinum 1617 CGI
instance: St. Francis of Paola (2030-04-02, "vide C5") for the plain
Confessor scheme, St. Basil the Great (2035-06-14) for the Bishop
scheme. The two turned out word-for-word identical in psalm numbers
and antiphon text across all three nocturns, differing only in their
three nocturn-closing versicles -- kept as separate full files since
the mechanism has no partial-override support.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 07:09:43 -04:00
will 0a8bcef859 Wire the Matins psalmody-override category tier into real resolution
Deploy / deploy (push) Successful in 1m5s
getMatinsPsalmodyOverride is now a genuine 3-tier lookup: proper
(saints/) -> Common (categories/, joined via the winner's own
SaintRecord.common) -> the plain ferial fallback, reversing the earlier
"category files are documentation-only" decision now that two
categories are trusted. Category files gain an `aliases` field;
common-of-a-martyr.yml lists common-of-a-martyr-bishop, since the
reference engine's own Matins psalmody redirects that Common outright
to plain Martyr — so its three Duplex+ saints (Ignatius, Callistus,
Boniface, none with proper text of his own) get real Common-tier
content with no new authoring.

tests/hours/matins.test.ts's old ferial-fallback proof (St. Ignatius)
is rewritten as a Common-tier proof; a fresh ferial-fallback proof
(St. John Bosco, an unauthored Confessor category) replaces it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 06:52:08 -04:00
will 9ba09964ea Author Common-of-a-Martyr/Apostle categories and St. Andrew's proper Matins psalmody
Deploy / deploy (push) Successful in 1m6s
Two categories live-verified: common-of-a-martyr (from St. Ignatius of
Antioch, whose own office is entirely Common — also confirms
Common-of-a-Martyr-Bishop redirects to this same scheme, and that none
of its 3 Duplex+ saints has a proper antiphon of his own) and
common-of-an-apostle (from St. Bartholomew). St. Andrew authored as the
second saint-level proof, sharing the apostle category's psalm numbers
in every slot with his own proper antiphon text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 06:43:00 -04:00
will f9a28ec001 Add Isaiah 61 and Wisdom 3/10 scripture chapters
Sparse transcriptions (only the cited verses) for St. Andrew's Matins
Nocturn III canticles, live-verified against the reference engine.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 06:42:54 -04:00
will 2bac28d36c Refactor Matins psalmody overrides to category-keyed data
Split the store into categories/*.yml (generic Common-category psalm-
number/versicle/canticle scheme) and saints/*.yml (per-saint proper
antiphon overrides, unchanged shape) per the mechanism's own flagged
next step. St. Lawrence's file moves into saints/ with a category
cross-reference; resolution behavior (per-saint lookup, ferial
fallback when unauthored) is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 06:42:48 -04:00
will f550412c61 Record the octave-precedence and Matins-psalmody fixes in TODO.md
Deploy / deploy (push) Successful in 1m7s
2026-08-18 06:11:57 -04:00
will 3e58f2258d Add Matins psalmody overrides for a Duplex+ weekday feast
matins.ts's three-nocturn gate correctly governed nocturn count
(Sunday, or Duplex+) but wrongly reused the literal Sunday psalmody as
content for every three-nocturn day, including a weekday feast. Live-
verified (Monastic Tridentinum 1617) that a Duplex+ weekday feast has
its own genuinely different psalmody -- confirmed per-Common-category,
not per-saint, by cross-checking St. Lawrence against St. Ignatius of
Antioch (a different Martyr sub-category, nearly identical psalm
numbers, different antiphons).

New mechanism (matins-psalmody-overrides.ts, mirroring the Lauds one
but keyed per-category) with St. Lawrence authored as the live-verified
proof; falls back to the plain ferial weekday table, redistributed into
3 nocturns, for every other Duplex+ id until its own category's content
is authored. Also generalizes the canticle shape to support a citation
spanning multiple scripture chapters under one heading (Lawrence's own
Nocturn 3), and adds the new Sirach/Jeremiah chapters his canticles cite.
2026-08-18 06:11:52 -04:00
will 1fc4bd7160 Fix octave-tie precedence, day-label commemorations, and rank display
A tie between an occurring saint's rank and an active octave now favors
the octave only on its own elevated closing day (live-verified: St.
Hyacinth vs. St. Lawrence's own Aug 17 closing day), not on an ordinary
octave day, where a tied saint still wins as before -- confirmed against
two already-tested counterexamples (St. Thomas of Canterbury, St.
Nicholas of Tolentino) that a blanket tie-flip would have broken.

getDayLabel previously dropped every commemoration whenever the day's
winner was a plain saint, and dropped every non-headline active octave
even when an octave itself won -- both fixed. Rank is now shown after
the day's own winner's name (previously not shown anywhere in the UI).

Also authored assumption-octave-day-3.yml, a real content gap (Aug 17,
day 3 of her octave) surfaced while fixing the above.
2026-08-18 06:11:37 -04:00
will ee3e155bfa Close out the octave-content TODO item
Deploy / deploy (push) Successful in 1m7s
Christmas Octave's own commemoration blocks are now modeled -- see the
prior two commits. Updates the top-of-file priority summary and the
dated build-log entry to reflect this.
2026-08-17 21:32:11 -04:00
will 6f1e152d7f Render octave commemorations in getDayCollects on stacking days
getDayCollects previously dropped kind: 'octave' commemorations
entirely, so on days where the temporal office keeps its own standing
(the Christmas Octave's own Nativity/Stephen/John/Holy-Innocents stack,
e.g. Dec 30) those octaves contributed nothing but a name in the day
label. Add an octave branch, gated on temporalCategory !== 'ordinary-
feria' -- the same condition resolveOfficeWinner uses -- so an ordinary
octave day's already-substituted primary content never gets a redundant
repeat.
2026-08-17 21:32:05 -04:00
will f60a142bb4 Add Christmas Octave's own commemoration collect content
Ant+V/R+collect "Commemoratio Octavæ" blocks for Christmas, St. Stephen,
St. John, and Holy Innocents, sourced from the reference engine's Sancti/
12-25..28.txt Octava 3 sections -- content for the mechanism change in
the next commit.
2026-08-17 21:31:55 -04:00
will 825328eae6 Add St. Francis Xavier (Dec 3) sanctoral content
Deploy / deploy (push) Successful in 1m7s
Own real Oratio and Nocturn 2-3 content (Duplex majus). This was the
only real gap left in December -- the rest of the month was already
complete from the earlier pre-1955 sanctoral pull. No test collisions:
Dec 3 was genuinely open.

This closes out the full-year sanctoral sweep (January-December).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 17:53:22 -04:00
will f7af1e7cbf Add November sanctoral content (5 saints/feasts)
Deploy / deploy (push) Successful in 1m6s
St. Charles Borromeo (11-04), the Octave Day of All Saints (11-08,
own real Cyprian/Augustine Nocturn 2-3 content, collect reused from
All Saints via cross-id propers), St. Albert the Great (11-15),
St. Gertrude (11-16), St. Felix of Valois (11-20). All Souls' Day
(11-02) deliberately skipped -- a Requiem office, not a normal
saint's office, a genuine mechanism gap.

Several new fixed-date Duplex+ saints displaced existing test
fixtures that had assumed particular dates were clean/ferial/
plain-Sunday: prime.test.ts's MONDAY constant, weekday.test.ts's
clean-Sunday case, and day-label.test.ts's post-Epiphany-overflow
and open-octave-day cases. Fixed by finding genuinely clean
alternate dates/years, never by weakening assertions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 17:49:06 -04:00
will 0955f7cbd1 Add October sanctoral content: 12 saints/feasts
Deploy / deploy (push) Successful in 1m9s
Continues the full-year sanctoral import: the Guardian Angels (Duplex
majus, full proper content), St. Thérèse of Lisieux, St. Bruno, St.
Bridget of Sweden, St. Francis Borgia, the Maternity of the BVM (Duplex
II Classis, instituted 1931 for the 1500th anniversary of the Council
of Ephesus, full proper content), St. Edward the Confessor, St. Teresa
of Avila (distinct from St. Thérèse of Lisieux -- two different
Carmelite saints sharing a name), St. Margaret Mary Alacoque, St. Peter
of Alcantara, St. John Cantius, and St. Raphael the Archangel (own
full proper content, matching st-gabriel-archangel.yml's established
pattern for a Sancti file that redirects wholesale to another date's
file but still carries its own real content).

St. Raphael's new fixed day (Oct 24, Duplex majus) collided with the
"clean, empty Saturday" fixture used by three tests for Marian
Saturday/Lauds-psalmody-override behavior; moved all three to 2026-07-04,
verified genuinely free of any sanctoral entry (not even a commemorated
Simplex).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 17:30:49 -04:00
will 2cf5e9cd39 Add September sanctoral content: 10 saints/feasts
Deploy / deploy (push) Successful in 1m7s
Continues the full-year sanctoral import: St. Stephen of Hungary, St.
Pius X, St. Lawrence Justinian, Ss. Protus and Hyacinth, the Holy Name
of Mary, the Seven Sorrows of the BVM (Duplex II Classis, own full
proper content except Nocturn 2, a real cross-reference to Passion
Friday's own temporal content), the Impression of the Stigmata of St.
Francis, St. Joseph of Cupertino, Our Lady of Ransom, and St.
Wenceslaus.

Ss. Protus and Hyacinth (Sep 11) were previously excluded under a
stale "real secondary saint blocked by the Nativity's own octave, same
as St. Clare in August" note -- wrong, same class of error already
corrected for St. Tiburtius/Susanna in August: the octave-window
exclusion was never a real rule. Sep 15 (day 8 of the Nativity's own
octave) is now also the Seven Sorrows' own fixed day (Duplex II
Classis), which wins outright with the octave still commemorated
alongside her -- updated september-sanctoral.test.ts's own "excluded
secondary saints" case accordingly, splitting it into the still-valid
Sep 9 (Gorgonius) half and a new Sep 15 case reflecting the real
winner.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 17:13:04 -04:00
will c4726c2953 Add August sanctoral content: 10 saints/feasts
Deploy / deploy (push) Successful in 1m9s
Continues the full-year sanctoral import: Ss. Tiburtius and Susanna, Ss.
Hippolytus and Cassian, the Vigil of the Assumption, St. Joachim (Duplex
II Classis, own full proper content), St. Hyacinth, St. John Eudes, St.
Jane Frances de Chantal, the Immaculate Heart of the BVM (Duplex II
Classis, instituted 1942, full proper content), St. Joseph Calasanctius,
and St. Raymond Nonnatus.

Several of the new fixed dates (Aug 16, 17, 21, 22) fall within or
adjacent to St. Lawrence's and the Assumption's overlapping octave
windows and now win outright there under the ordinary-feria/ordinary-
sunday precedence rules, displacing what several existing tests
expected to be bare octave commemorations or ferial fallbacks. Updated:

- august-sanctoral.test.ts: Aug 22 now correctly shows the Immaculate
  Heart of Mary as the winner, with the Assumption's octave still
  commemorated alongside her.
- day-label.test.ts: the two octave-priority tests pinned to Aug 17
  (now St. Hyacinth's own day) moved their "day 8 alone" case to All
  Saints' own octave (Nov 8, still open); the two-octaves-overlap
  comparison can no longer be demonstrated via any real date in this
  app since every day where St. Lawrence's and the Assumption's octaves
  genuinely overlap is now real sanctoral content -- the underlying
  `pickWinningOctave` mechanism itself stays covered by
  tests/calendar/octaves.test.ts's own synthetic-data unit test.
- vespers.test.ts: the Thursday Ps 138 split test moved off Aug 20,
  whose own first Vespers is now claimed by St. Jane Frances de
  Chantal's fixed day (Aug 21).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 16:59:17 -04:00
will 22917c0068 Add July sanctoral content: 11 saints/feasts, fix St. Leo II's wrong date
Deploy / deploy (push) Successful in 1m6s
Continues the full-year sanctoral import: the Most Precious Blood of Our
Lord (Duplex I Classis, full proper office, elevated by Pius XI for the
1933 Redemption jubilee), the Visitation of the BVM (Duplex II Classis,
full proper office), St. Leo II, St. Anthony Mary Zaccaria, the Octave
Day of Ss. Peter and Paul (own real content, not a bare continuation),
Ss. Cyril and Methodius, St. Elizabeth of Portugal, St. Henry, Our Lady
of Mount Carmel, St. Vincent de Paul, and St. Ignatius of Loyola.

Also fixes a real pre-existing bug surfaced while authoring St. Leo II:
an earlier June sanctoral pull had mapped him to "06-28" (his own vita
text mentions his burial "upon the 28th day of June," but his actual
liturgical feast, per the reference engine's Sancti/07-03.txt, is July
3) with no saints/st-leo-ii.yml ever authored for it -- a wrong-date
stub, same class of artifact as the pre-existing St. Apollinaris
wrong-date bug already documented in july-sanctoral.test.ts. Removed
the stale June 28 entry now that he's correctly authored on July 3.

Two day-label tests pinned to Trinity-Sunday-relative dates that now
collide with new fixed feasts (Jul 6: Octave Day of Ss. Peter and Paul)
were moved to collision-free years/dates, and the July stub test
covering "pure octave-continuation days" was updated to a date (July 4)
that's still genuinely unmapped, since most of that range turned out to
have real content after all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 16:43:28 -04:00
will 0b8529e04b Add June sanctoral content: 6 saints
Deploy / deploy (push) Successful in 1m4s
Continues the full-year sanctoral import: St. Angela Merici, St. Francis
Caracciolo, St. Boniface of Mainz (Bishop and Martyr, Apostle of Germany --
distinct from the earlier, unrelated St. Boniface Martyr already on May 14),
St. Norbert, St. Margaret of Scotland, and St. Aloysius Gonzaga.

Two of the new fixed dates (June 1, June 4) fall on dates used by two
day-label tests pinned to specific Trinity Sunday years; moved those to
years/dates free of the new entries, with explanatory comments.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 16:23:05 -04:00
will b4e6ed6aa0 Add May sanctoral content: 10 saints plus the Queenship of the BVM
Deploy / deploy (push) Successful in 1m6s
Continues the full-year sanctoral import (see TODO.md/memory for Jan-Apr):
St. Pius V, St. Antoninus, St. Robert Bellarmine, St. John Baptist de la
Salle, St. Paschal Baylon, St. Venantius, St. Bernardine of Siena, St. John
Baptist de Rossi, St. Augustine of Canterbury, St. Mary Magdalene de Pazzi,
and the Queenship of the BVM (May 31, Duplex II Classis, instituted by Pius
XII's 1954 "Ad cæli Reginam" — full proper content, not a Common fallback).

Adding the Queenship to May 31 surfaces a real collision with Trinity
Sunday in years where Trinity falls on May 31 (2026): since this app
models Trinitytide Sundays as plain ordinary-sunday with no special
first-class standing, a Duplex+ sanctoral feast wins outright there per
the existing precedence rule. Updated the two tests pinned to that date
accordingly, with a third moved to a Trinity Sunday date (2023-06-04)
free of any sanctoral entry to keep testing the octave-vs-anchor-day
interaction in isolation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 16:13:19 -04:00
will 253a6ce516 Add 6 more April saints previously never added
Deploy / deploy (push) Successful in 1m5s
St. Isidore of Seville, St. Vincent Ferrer, St. Hermenegild, St. Anselm,
St. Fidelis of Sigmaringen, St. Peter Canisius -- April complete.
St. Peter Canisius is the second common-of-a-confessor-doctor saint
(after St. John Damascene in March), same benedictusCommon reuse
pattern. St. Hugh, Abbot (Apr 1) checked and correctly skipped: his own
file is Commemoratio-rank with only a collect, no Lectio content at
all to source.

npm test (365 tests) and tsc --noEmit both pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 15:51:01 -04:00
will 9b06896391 Add St. Gabriel Archangel, the Annunciation, St. John Damascene,
Deploy / deploy (push) Successful in 1m5s
St. John Capistrano -- March complete

Finishes March's sweep of previously-never-added sanctoral content.
The Annunciation (Mar 25) was one of the two feasts literally named in
this app's original stale "fixed BVM feast, excluded per the usual
convention" note (alongside Purification, already fixed).

St. John Damascene is the first common-of-a-confessor-doctor saint;
his own Commune/C5a points its own Benedictus antiphon back at C4a's,
so he reuses the already-authored
benedictus-antiphon-common-of-a-confessor-bishop.yml directly rather
than duplicating it.

npm test (365 tests) and tsc --noEmit both pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 15:41:44 -04:00
will aa19e1dc04 Add Ss. Perpetua and Felicity, St. John of God, St. Patrick, St. Cyril
Deploy / deploy (push) Successful in 1m7s
of Jerusalem (March)

Continuing the sweep of previously-never-added sanctoral content. New
shared Common templates: collect-c7b.yml and
benedictus-antiphon-common-of-several-women-martyrs.yml (Commune/C7b,
"several women Martyrs, not Virgins" -- Perpetua and Felicity's own
Common category, not authored before now).

St. Gabriel Archangel (Mar 24), the Annunciation (Mar 25), St. John
Damascene (Mar 27), and St. John Capistrano (Mar 28) are still open --
substantial feasts left for a follow-up pass.

npm test (365 tests) and tsc --noEmit both pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 15:33:10 -04:00
will ce778d51e3 Add 6 more February saints/feasts previously never added
Deploy / deploy (push) Successful in 1m6s
Purification of the BVM (Feb 2), St. Andrew Corsini (Feb 4), St. John
of Matha (Feb 8), Apparition of the Immaculate BVM at Lourdes (Feb 11),
Ss. Seven Founders of the Servite Order (Feb 12), St. Gabriel of Our
Lady of Sorrows (Feb 27) -- all had real, complete Matins content in
the reference engine and no exclusion comment; simply never reached
during the earlier month-by-month sanctoral pulls. Continuing the same
sweep as January's St. Francis de Sales/St. Martina/St. John Bosco.

Test update (not a regression): St. Andrew Corsini transfers onto Feb 5
in years where Sexagesima falls on his own Feb 4, outranking St. Agatha
when it does -- february-sanctoral.test.ts's proof year for Agatha
moved from 2029 (now a collision year) to 2025 (clean).

npm test (365 tests) and tsc --noEmit both pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 15:26:00 -04:00
will db141681a9 Add St. Francis de Sales, St. Martina, St. John Bosco (Jan 29-31)
Per direct instruction to import all available sanctoral content
regardless of rubric-era markers: these three had real, complete Matins
content in the reference engine and were simply never added during the
earlier month-by-month sanctoral pulls (no exclusion comment, no
placeholder -- just never reached). First of a broader sweep across the
whole calendar.

New shared Common template: benedictus-antiphon-common-of-a-confessor.yml
(Commune/C5's own Ant 1) -- the first common-of-a-confessor saint that
needed one; st-alexius.yml (the only prior user of this category) had
used a Cistercian-specific alternate instead.

npm test (365 tests) and tsc --noEmit both pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 15:10:56 -04:00
will 80f4f935be Add 4 more fixed Lord's/BVM feasts, and fix Our Lady of the Snows' Nocturn 3
Deploy / deploy (push) Successful in 1m6s
Finding of the Holy Cross (May 3), the Transfiguration (Aug 6), the
Exaltation of the Holy Cross (Sep 14), and Dedication of the Basilica of
the Most Holy Savior (Nov 9, the Lateran) were all excluded under the
same stale "fixed feast of the Lord, not modeled" note that turned out
wrong for Our Lady of the Snows. All four have real, complete content in
the reference engine. Presentation of the BVM (Nov 21) was excluded
under the equivalent "fixed BVM feast" note and is added the same way.

- finding-of-the-holy-cross: fully proprium, own collect/antiphon/Matins
  throughout; now correctly outranks
  ss-alexander-eventius-theodulus-and-juvenal, matching what that
  saint's own file already documented as the live reality.
- transfiguration: same proprium shape. Its subordinate saints (Ss.
  Xystus/Felicissimus/Agapitus) stay excluded -- blocked by the real,
  separate octave/subordination-rendering gap, not by an unmodeled
  Transfiguration.
- exaltation-of-the-holy-cross: reuses the Finding of the Holy Cross's
  content wholesale (ex Sancti/05-03) -- collect/antiphon via a cross-id
  `propers` pointer, Matins nocturn-readings duplicated (no cross-id
  lookup exists for that mechanism).
- dedication-of-the-basilica-of-the-most-holy-savior: reuses the
  existing Common-of-a-Dedication (collect-c8, benedictus-antiphon-
  common-of-a-dedication), real unique Nocturn 2 vita of her own. Her
  secondary, St. Theodore, stays excluded for the same
  octave/subordination reason as above.
- presentation-of-the-bvm: real unique collect of her own; her
  Benedictus antiphon is a substituted Common reference
  (truncate-and-add-alleluia edit specific to this feast), authored as
  her own file.

Also: our-lady-of-the-snows.yml was missing its own Nocturn 3 (she
defers to Commune/C11's own Gospel-incipit-plus-Bede-homily, same as
Presentation of the BVM) -- added.

Test updates (not regressions): three tests had fixed proof dates
colliding with the newly-added feasts, updated to reflect the new,
correct behavior. npm test (365 tests) and tsc --noEmit both pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 14:48:50 -04:00
will 42d0b21c2a Add Our Lady of the Snows (Aug 5) to the calendar
Deploy / deploy (push) Successful in 1m5s
Previously excluded entirely under a stale "fixed BVM feasts aren't
modeled" note -- wrong: this app already models three other fixed BVM
feasts (assumption, immaculate-conception, nativity-bvm), and this one
has real, complete content too. Sancti/08-05.txt's untagged rank block
gives her Duplex Majus with real Nocturn 2/3 content (the
miraculous-snowfall founding legend of St. Mary Major); SanctiM/08-05.txt
confirms she's on the Monastic 1617 track too, reslicing the same
Lectio4-6.

Unlike the other three BVM feasts, she has no proper collect or
Benedictus antiphon of her own (her Rule says "ex C11" -- Common of
Feasts of the BVM) -- collect-c11.yml and
benedictus-antiphon-common-of-the-bvm.yml are real, reusable Common
templates, wired via collectCommon/benedictusCommon like any other
Common-only saint, not a private per-saint duplicate.

Live-checked via direct resolution: she wins outright on 2026-08-05 and
2027-08-05, with antiphon, collect, and Matins reading all resolving
verified. npm test (365 tests) and tsc --noEmit both pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 14:21:14 -04:00
will 99617bcea5 Restore missing English sentence in Aug 5 martyrology entry
The Latin text already named the dedication of St. Mary Major ("Romæ,
in Exquiliis, Dedicatio Basilicae sanctae Mariae ad Nives") but the
English translation silently dropped the corresponding sentence,
jumping straight to the day's other martyrs. Restored from the
reference engine's own English/Martyrologium/08-05.txt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 14:20:58 -04:00
will b076e77e9c Author octave-absorbed and Vigil-only Matins readings, correcting earlier policy
Deploy / deploy (push) Successful in 1m7s
st-telesphorus, st-hyginus (Jan 5/11, octave-absorbed — their day's Nocturn
2/3 is given over to a Nativity/Epiphany homily rather than their own vita)
and vigil-of-st-john-the-baptist, vigil-of-st-lawrence (Vigils whose single
3-lesson nocturn was misread as "no content") were left unauthored earlier
in the SanctiM pass on the theory that non-proper-to-the-saint text
shouldn't be sourced. Per direct correction: this app pools every available
reading first and figures out display later, so real sourceable text
belongs in the pool even when it isn't proper to that particular saint —
flagged as such in each file's own header, not disguised as the saint's
own vita. vigil-of-all-saints was re-checked under the same policy and
confirmed still genuinely contentless (a bare @Commune rubric).

npm test (365 tests) and tsc --noEmit both pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 13:06:53 -04:00
will 58816a7d15 Add December Matins readings for SanctiM-track saints, completing the pass
Deploy / deploy (push) Successful in 1m7s
13 of December's 15 SanctiM-track saints authored (st-bibiana, st-nicholas,
st-ambrose, immaculate-conception, st-damasus, st-lucy, st-thomas-apostle,
st-stephen-protomartyr, st-john-apostle, holy-innocents, st-thomas-becket,
st-silvester-i, st-eusebius-of-vercelli); st-barbara and st-melchiades left
unauthored as Common-only. This is the twelfth and final month of the
SanctiM (Monastic 1617)-track saints' Matins readings pass — 129 saints
authored across the whole year.

Adding St. Eusebius of Vercelli's content surfaces a 3rd Matins lesson on
the existing Advent III Tuesday proof date (Dec 15 falls under his
commemoration); updated matins.test.ts accordingly — the "commemorations
get their own reading" design working as intended, not a regression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 07:14:31 -04:00
will 42ee9f816a Add November Matins readings for SanctiM-track saints
Deploy / deploy (push) Successful in 1m8s
14 of November's 16 SanctiM-track saints authored, including All
Saints of the Benedictine Order -- a genuinely Monastic-only feast
with both rich content and (unusually for this pass) an available
English translation. St. Martin I is another off-date case like St.
Paul the Hermit, and two more saints (St. Emilian, St. Odo) turn out
Monastic-only-but-Common-only, extending October's wrinkle.
2026-08-17 06:54:11 -04:00
will 035d9c9fae Add October Matins readings for SanctiM-track saints
Deploy / deploy (push) Successful in 1m9s
9 of October's 13 SanctiM-track saints authored. Found a new wrinkle:
St. Justina and St. Gall are genuinely Monastic-only (no Tridentine
file at all) but their own SanctiM files turn out to be Common-only
with no proper Lectio content -- being Monastic-only doesn't guarantee
real text to transcribe.
2026-08-17 06:40:50 -04:00
will 7c5aaed317 Add September Matins readings for SanctiM-track saints
Deploy / deploy (push) Successful in 1m10s
All 9 of September's SanctiM-track saints authored -- the first full
month in this pass with nothing left unsourced. Includes the
Dedication of St. Michael the Archangel, whose SanctiM file borrows
structure from his separate May 8 Apparition file but still resolves
its actual readings back to this day's own Tridentine content.
2026-08-17 06:33:55 -04:00
will abab080bef Add August Matins readings for SanctiM-track saints
Deploy / deploy (push) Successful in 1m6s
13 of August's 16 SanctiM-track saints authored, including the Duplex
I class feasts of St. Lawrence and the Assumption of the B.V.M. The
Assumption's presence confirms this app's calendar already maps fixed
BVM feasts, despite an older note in the parent workspace CLAUDE.md
suggesting otherwise -- flagged in memory as stale documentation to
verify against, not trust blindly. 3 saints left unsourced (Common-only
or Nocturn-1-only vigil content).
2026-08-17 06:26:44 -04:00
will 2cecda3a6e Add July Matins readings for SanctiM-track saints
Deploy / deploy (push) Successful in 1m6s
13 of July's 18 SanctiM-track saints authored (5 left unsourced,
Common-only). Discovered the Monastic calendar keeps a full Octave of
St. Benedict (Jul 11-18), which explains several dates' SanctiM files
not matching their expected saint. Also hit a translation-misalignment
bug on St. Bonaventure's English source (paragraph boundaries don't
line up with the Latin), worked around with an original translation.
2026-08-17 06:14:00 -04:00
will 1de62e698d Add June Matins readings for SanctiM-track saints
Deploy / deploy (push) Successful in 1m9s
12 of June's 13 SanctiM-track saints authored, including the major
Duplex I class feast of Ss. Peter and Paul. One left unsourced (the
Vigil of St. John the Baptist has only a Nocturn 1 scripture reading
in the source, nothing proper to author for Nocturn 2/3). Found a new
wrinkle: St. William Abbot has real vita content in both tracks,
essentially the same story independently transcribed — sourced from
the Tridentine file since only it has an English translation.
2026-08-17 06:01:40 -04:00
will 127eb61879 Add May Matins readings for SanctiM-track saints
Deploy / deploy (push) Successful in 1m11s
12 of May's 13 SanctiM-track saints authored (1 left unsourced,
Common-only). Resolves an old loose end from the pre-1955 pass: St.
Romanus Abbot does have a real SanctiM file after all, it's just
Common-only with no proper Lectio content, which looked like a missing
file until actually opened. Also the first isGospel:true case since
January (Ss. Philip and James's Nocturn 3 has a full Gospel pericope,
not a bare incipit).
2026-08-17 05:49:20 -04:00
will 10c10fdaf4 Add April Matins readings for SanctiM-track saints
Deploy / deploy (push) Successful in 1m9s
All 9 of April's SanctiM-track saints authored. Found a new wrinkle:
St. Catherine of Siena and St. Peter Martyr sit on swapped dates
between this app's calendar and the general Tridentine one (Apr 29/30
each way), not just an off-by-a-few-days shift like earlier cases.
Also hit a reference-engine data bug on St. George (English prose
filed under a Latin heading) and another genuinely Monastic-only saint
(St. Robert of Molesme) needing an original translation.
2026-08-17 05:38:54 -04:00
will 7de09f2865 Add March Matins readings for SanctiM-track saints
Deploy / deploy (push) Successful in 1m11s
All 6 of March's SanctiM-track saints authored. St. Benedict's own
patronal feast turns out to be the richest case in this pass so far:
its Nocturn 3 is genuinely Monastic-only content (a St. Peter Damian
sermon) absent from the Tridentine track entirely, unlike every other
saint done so far where the SanctiM file just reslices the same
Tridentine text. Also caught a case (St. Gregory the Great) where a
saint's SanctiM file carries content copied verbatim from an unrelated
feast rather than anything proper to the saint himself.
2026-08-17 05:31:34 -04:00
will ff5cb601aa Add February Matins readings for SanctiM-track saints
Deploy / deploy (push) Successful in 1m4s
8 of February's 10 SanctiM-track saints authored (2 left unsourced —
Common-only, no proper text in the source). Two more turned out to be
missed pre-1955-pool candidates rather than genuine SanctiM cases
(their date's SanctiM file belongs to a different saint sharing the
date). Also settles a convention for bare Gospel incipits followed by
a real homily: author the homily alone as isGospel:false rather than
including the incipit line under isGospel:true.
2026-08-16 21:48:38 -04:00
will 0b28792513 Add January Matins readings for SanctiM (Monastic 1617)-track saints
Deploy / deploy (push) Successful in 1m5s
Starts the bulk-content pass complementary to the pre-1955 saints pass:
11 of January's 13 SanctiM-track saints authored (2 left unsourced —
their Nocturn 2/3 content is entirely octave homiletic material, no
proper vita text in either track). Most SanctiM files turn out to be
thin rule overrides pointing back at the same Sancti/ text; only
St. Maurus (Monastic-only, no Sancti file) needed direct SanctiM
sourcing, with an original translation for its missing English source.
2026-08-16 21:38:52 -04:00
will 9feaf37d25 Document the pre-1955 sanctoral Matins readings pass in TODO.md
Deploy / deploy (push) Successful in 1m6s
Removes the resolved "mechanism gap" entry (it turned out to need no
new mechanism, just sourcing), adds a dated build-log entry with the
sourcing method and the critical "plain Sancti/MM-DD.txt file is often
the wrong saint" wrinkle, and repoints the remaining backlog at the
larger Monastic-1617-track saint pool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 21:12:24 -04:00
will 7cf93e7398 Add November Matins readings for pre-1955 sanctoral saints
Ss. Tryphon and Companions, St. Gregory Thaumaturgus, St. Catherine of
Alexandria, and the Vigil of St. Andrew. This completes the
pre-1955-but-post-1617 sanctoral pass: 43 saints authored across the
year, 9 confirmed Common-only with nothing to source (see TODO.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 21:12:14 -04:00
will d66740b1d0 Add October Matins readings for pre-1955 sanctoral saints
St. Dionysius and Companions, and St. Evaristus.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 21:12:07 -04:00
will 3bfcf6efcb Add September Matins readings for pre-1955 sanctoral saints
St. Giles, St. Nicholas of Tolentino, Ss. Cornelius and Cyprian, Ss.
Euphemia/Lucy/Geminian, St. Januarius, the Vigil of St. Matthew, and
Ss. Cyprian and Justina.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 21:12:01 -04:00
will 85ac0efea5 Add August Matins readings for pre-1955 sanctoral saints
The Finding of St. Stephen, St. Donatus, St. Louis, St. Zephyrinus, and
Ss. Felix and Adauctus.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 21:11:53 -04:00
will d097deb6a7 Add July Matins readings for pre-1955 sanctoral saints
St. Praxedes, Ss. Nazarius/Celsus/Victor/Innocent, and St. Pantaleon.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 21:11:47 -04:00
will 594c86618e Add June Matins readings for pre-1955 sanctoral saints
Ss. Basilides/Cyrinus/Nabor/Nazarius, St. Anthony of Padua, Ss. Gervase
and Protase, St. Silverius, and St. Paulinus of Nola.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 21:11:41 -04:00
will 0ade066bf9 Add May Matins readings for pre-1955 sanctoral saints
St. Monica, St. Stanislaus, the Apparition of St. Michael, and St.
Eleutherius. St. Monica's Nocturn 3 Gospel+homily reuses the
already-verified Luke 7:11-16 text from post-pentecost-15.yml, the same
pericope her own day's lessons cross-reference.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 21:11:34 -04:00
will d84671273a Add April Matins readings for pre-1955 sanctoral saints
Sts. Francis of Paola, Anicetus, and Soter and Caius.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 21:11:28 -04:00
will 04a895101b Add February Matins readings for pre-1955 sanctoral saints
Sts. Blaise, Apollonia, and Faustinus and Jovita.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 21:11:22 -04:00
will 085062e697 Add January Matins readings for pre-1955 sanctoral saints
Sts. Marcellus I, Paul the Hermit, Marius/Martha/Audifax/Abachum,
Emerentiana, Timothy, Polycarp, and Agnes (second feast) — sourced from
the reference engine's Tridentine/Divino Afflatu track since none of
these saints existed in the Monastic 1617 calendar this app otherwise
follows.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 21:11:16 -04:00
will a7901b3c61 Implement the resumed-post-Epiphany-Sunday temporal-id algorithm
Deploy / deploy (push) Successful in 1m6s
Post-Pentecost overflow years (early Easter, fewer than 6 Sundays after
Epiphany fit before Septuagesima) previously clamped every excess Sunday
to post-pentecost-24 instead of resuming the skipped post-Epiphany
Sundays' own content, per the traditional rubric. Ported directly from
the reference engine's own DivinumOfficium/Date.pm getweek().

Also adds the Epiphany VI Saturday-before-Septuagesima commemoration
(only when Epiphany V was the last Sunday to actually occur), and fixes
day-label.ts's on-screen display to track the same resumed-Sunday
content instead of a raw elapsed-week count — including a correction so
the fixed final Sunday of the year always reads "23rd Sunday after
Trinity", not a per-year-varying number.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 20:07:04 -04:00
will f811fcc8f6 Correct TODO.md: bible-plan content has no reference breviary to verify against
The user's own reading plan isn't sourced from a historical breviary, so
"not yet spot-verified against a reference" wasn't a real gap for this
content, unlike sanctoral/temporal data. Also confirms Jn 17 on Sunday
after Ascension as a deliberate, remembered exception.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 19:24:24 -04:00
will d8d18fe31a Bulk-convert the bible-plan TSV into Matins reading data
Converts all ~369 non-blank rows of the user's own scripture-reading
plan TSV into data/hours/bible-plan/*.yml, mapping each row's day label
to this app's (temporalId, weekday) or Dec25-Jan13 calendarDate scheme.
Fixes a few source TSV typos in the process (chapter-range and
trailing-colon citations); status stays explicitly `missing` pending the
separate Vulgate/Douay-Rheims import.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 19:20:18 -04:00
will 2cc856ed92 Rework Matins readings into a pooled, distributed model
Replaces the fixed Nocturn 1 = plan / Nocturns 2-3 = patristic slotting
with one ordered pool (scripture plan, winner/commemorations/temporal id,
active octaves) sliced evenly across however many nocturns the day has.
Also makes the bible-plan store dual-keyed so the Dec 25 - Jan 13 stretch
can key off a fixed calendar date alongside the usual (temporalId,
weekday) key. Mechanism only — content authoring is still 2 proof dates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 07:06:23 -04:00
will 915c441170 Add README
Deploy / deploy (push) Successful in 1m2s
Explains what vu is and how to develop and run it in production, without referencing reground.org's specific infrastructure.
2026-08-16 06:30:00 -04:00
will 5c0664081b Add a generic production docker-compose.yml
Lets someone outside Reground's infra run vu standalone: builds the image and exposes it on localhost:8080, independent of the reground.org-specific compose config that joins an external network with no published port.
2026-08-16 06:29:56 -04:00
will 2c7ab3419d Ignore CLAUDE.md, MEMORY.md, and .claude/
These are Claude Code working files, not project documentation meant to ship in the repo.
2026-08-16 06:29:52 -04:00
will b0d1cb2807 Document Matins mechanism build in TODO.md
Deploy / deploy (push) Successful in 1m0s
2026-08-14 11:40:56 -04:00
will 974c156255 Add Matins tests; update tests that assumed it was still a stub
tests/hours/matins.test.ts covers both branches (ferial, Sunday)
end-to-end through the real resolveOrdo mechanism against two clean,
live-verified dates — psalm/canticle counts, Te Deum presence, reading
citations/statuses, Gospel flagging and its Common-fallback exclusion,
and responsory matching.

tests/hours/prime.test.ts and tests/ui/shell.test.ts both had assertions
that Matins was still pending — replaced with assertions that it now
renders real content, mirroring the equivalent Compline test.
2026-08-14 11:40:51 -04:00
will bedbf577ea Build Matins' real dynamic ordo, wire into the UI
hours/matins.ts no longer a stub: resolves a full ordo assembled
programmatically per day rather than a static parts list, since the real
structure (1 nocturn ferial vs. 3 on a Sunday or Duplex+ feast, a
variable number of readings) doesn't fit the pattern every other hour
uses. Several deliberate departures from the historical office, all per
direct instruction (see TODO.md's own writeup): Nocturn 1 always draws
from the user's bible-plan rather than the historical lectionary and
never contracts for "summer"; split historical lessons are recombined
except where the source genuinely changes; Nocturn 3 is gated at
Duplex-and-higher plus every Sunday; every commemorated saint and active
octave contributes its own reading, not just the office winner; a Gospel
reading is sourced only from the user's plan or the day's own proper
content, never a Common-of-Saints fallback; the Invitatory is framed like
an ordinary antiphoned psalm rather than the historical repeating
refrain.

ui/hour-view.ts renders the two new part kinds (nocturn-psalmody,
te-deum) and the extended lesson shape (responsory, Gospel flag).
2026-08-14 11:40:43 -04:00
will 83bf55efdb Add reusable Matins building blocks: types, content stores, seed data
New ResolvedPart kinds (nocturn-psalmody, te-deum) and lesson fields
(nocturn, source, isGospel, responsory) in hours/types.ts. Three new
content-store modules: propers/bible-plan.ts (the user's own continuous
scripture-reading plan, replacing the historical Nocturn-1 lectionary),
propers/nocturn-readings.ts (general feast/temporal-day Nocturn 2-3
readings, extending propers/octave-readings.ts's combine-into-one-reading
pattern via a newly-exported resolvePassages), and
propers/matins-responsories.ts (a per-book responsory pool, matched
loosely rather than by exact citation).

Data: Sunday's fixed 12-psalm/3-canticle psalmody (live-verified against
the reference engine), the invitatory antiphon, ferial hymn and
capitulum, the Te Deum, a seeded Isaiah responsory, two live-transcribed
scripture chapters, and the two bible-plan rows needed for this pass'
proof content.
2026-08-14 11:40:31 -04:00
will 5162eabdb4 Document Vespers' ferial Preces and per-feast office overrides in TODO.md
Deploy / deploy (push) Successful in 59s
Records what shipped in the previous two commits, plus the known gaps
(5 hymns with a seasonally-variable doxology not modeled, same class
as the Paschaltide alleluia suffix elsewhere in this file).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:32:13 -04:00
will 53582f1ded Add per-feast Vespers office overrides for all 30 duplex-majus+ ids
Same eligibility rule as Lauds' psalmody override (a duplex-majus+
saint, or a named temporal feast), factored out to
resolve-common.ts's getOfficeOverrideId since Vespers has no
psalmody-override table of its own to piggyback eligibility on.

Authored via one live command=prayVespera query per id, reusing the
same clean per-saint dates already established for each one's Lauds
office-bundle override. The capitulum turned out to be byte-identical
between Lauds and Vespers for every one of the 30 ids checked, a real
Monastic-rite fact rather than a coincidence — so no
vespers-capitulum-<id>.yml files exist at all; the chapter falls
through to the already-authored lauds-capitulum-<id> directly instead
of duplicating identical content.

St. Scholastica's Vespers responsory, hymn, and versicle needed a hand
translation (same gap she has at Lauds/Terce/Sext/None — the reference
engine has no English for any of her proper content). Two more
one-line English gaps in the shared Immaculate Conception/Nativity of
the BVM responsory were filled with the standard Douay-Rheims/Ave
Maria wording.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:31:59 -04:00
will 4d88568c42 Add Vespers' ferial Preces, sourced from Tridentine 1906/1910
Monastic 1617 has no ferial Preces at Vespers, same gap Lauds already
had — the fuller "Preces feriales Vespera" is byte-for-byte the Lauds
ferial Preces with Psalm 129 swapped for Psalm 50 in the source, live-
verified against the same clean Advent feria already used for Lauds.
The short Sunday/feast form turns out to be identical at both hours too,
so it's reused directly rather than duplicated.

Factors the shared ferial-or-vigil predicate out of hours/lauds.ts into
resolve-common.ts's isFerialOrVigil so both hours' -preces cases resolve
it the same way instead of keeping two copies in sync by hand.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:30:01 -04:00
will ce954fea9a Update TODO.md: Vespers ordo is built, not a stub
Deploy / deploy (push) Successful in 57s
Records what shipped last session and what it still owes (its own
ferial Preces, a per-feast office-override mechanism).
2026-08-13 20:43:08 -04:00
will f14d00bd73 Port seasonal office content to Lauds, closing the same pre-existing gap
Deploy / deploy (push) Successful in 56s
Lauds had the identical issue Vespers started with: resolveOffice only
ever rendered the plain weekday chapter/responsory/hymn/versicle,
year-round, with no Advent/Lent/Passiontide/Paschaltide override —
same source file (Major Special.txt) as Vespers', just never wired up.
Ports the same seasonalOfficeSuffix-based precedence (per-feast/octave
override -> season -> weekday default) and the matching seasonal
content, transcribed the same way as Vespers' own (Monastic hymn
variants applied from the source's substitution notation against each
base Roman text). Paschaltide's chapter is byte-identical to Vespers'
own — the source aliases "[Pasch Vespera] = @:Pasch Laudes" — so both
hours' data files intentionally carry the same text rather than one
importing the other's id.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 20:15:29 -04:00
will de83786793 Build the Vespers ordo: mechanism, weekday content, seasonal content
Wires resolveOrdo('vespers', ...) end to end: opening versicle,
weekday psalmody (4 groups, handling the joined Ps 115+116 and the
split Ps 138/143 via a PsalmRef-like shape), the chapter/responsory/
hymn/versicle office bundle, Magnificat, day-collects — reusing
resolveEveningDay so First/Second Vespers precedence and the cross-day
commemoration (both added in the previous commit) apply automatically
— and a closing that reuses Lauds' own conclusio, since no distinct
"Vespera" version of that exact text turned up in the source.

getMagnificatAntiphon mirrors getBenedictusAntiphon's saint/temporal-id
lookup order, but also falls back to real content Benedictus has no
equivalent of: the classic ferial set that quotes the Magnificat's own
text in sequence across the week, with the correct Monastic-rubric
variant picked for Tue/Thu/Fri/Sat (two of those needed a fresh
translation where the Monastic Latin diverges from the given English
in content, not just wording).

Also includes Vespers' own seasonal content (Advent/Lent/Passiontide/
Paschaltide chapter+responsory+hymn+versicle), transcribed from the
same source blocks as the weekday content, with the Monastic-specific
hymn variants hand-applied from the source's substitution notation
against each base Roman text (one transcription slip caught and fixed
mid-pass: the Passiontide hymn's closing lines had been copied from
the Roman text instead of the Monastic one actually being
transcribed). Passiontide's own responsory/hymn correctly omit the
Gloria Patri, matching real rubric and the source data as given.

All weekday/seasonal content marked `draft`, not `verified` — read
directly from the reference source, not cross-checked against a live
rendering per day the way the psalmody was.

Six other hours' resolvePart switches needed the three new HourPart
kinds ('vespers-psalmody', 'vespers-office', 'magnificat') added to
their exhaustive not-used lists so they still typecheck; three
pre-existing tests assumed Vespers was still "coming soon" and are
updated to exercise Matins (still the genuine stub) instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 20:14:19 -04:00
will 979bbe6aa4 Model cross-day Vespers commemorations, fix First Vespers threshold
Deploy / deploy (push) Successful in 1m1s
Whichever of today/tomorrow doesn't govern a given evening's Vespers
can still be commemorated there, unless it's Simplex (or a bare
temporal winner with no standing of its own) — live-verified in both
directions against the real Monastic 1617 engine: Ss. Cyriacus/Largus/
Smaragdus (Semiduplex) commemorated when Sunday claims First Vespers
over them; St. Anne commemorated even though St. James (Duplex II.
classis) keeps his own Second Vespers outright; St. Donatus (Simplex)
gets nothing ("nihil de præcedenti") in the same situation Cyriacus/
etc. did get a commemoration. The isMajorFixedFeastOfTheLord override
(Christmas/Epiphany/Candlemas/Easter/Ascension/Corpus Christi/Sacred
Heart) deliberately doesn't inherit this yet — no live data on whether
those suppress it the way privileged-sunday/privileged-feria-major do
during the day.

Also fixes hasFirstVespers's threshold, found wrong while gathering
the above evidence: it required duplex-majus+, but Ss. Cyriacus/etc.
(plain Semiduplex) actually claims First Vespers over St. Donatus in
the live engine. Lowered to semiduplex, matching the sibling
privileged-feria-minor threshold already used just below it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 18:59:16 -04:00
will 3aee8eca2f Bulk-import the full Psalter from Divinum Officium's Psalmorum text
Rather than continuing to author psalms one at a time as each hour
needs them, pull all 150 from the reference corpus directly (Latin +
English line up 1:1 for every psalm) — Vespers alone needed ~25 new
numbers, and Matins will eventually need most of the rest anyway.
Also completes 001.yml, which turned out to still be a 2-verse
placeholder predating this project's "verified" convention.

Ps 94 (the Invitatory) is included too, with its source file's
repeating "$ant" antiphon macro dropped — this app's Matins doesn't
need the interleaved-antiphon structure the real Invitatorium uses, so
it's stored as plain continuous verses like every other psalm (a
deliberate simplification, not an oversight). Its English is the
traditional Coverdale/BCP "Venite" translation rather than this
store's usual Douay-Rheims register, so it's marked `draft` pending a
deliberate reword later.

prime.test.ts's status-surfacing test relied on Ps 1 being incomplete;
rewritten against Ps 94's real verified/draft split and Ps 151 (off
the real 1-150 range) for the missing-fallback case instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 18:59:01 -04:00
will b73f8e108a Transcribe Monastic Vespers weekday psalmody from source
Sourced from Divinum Officium's explicit "[Monastic Vespera]" block
(Latin+English Psalmorum), not the ambiguous unlabeled Day/Daya tables
earlier in the same file — confirmed by cross-checking Monday's psalms
against the already live-verified Lauds antiphons, which the unlabeled
tables both got wrong. Only 4 psalms a day (Rule of Benedict ch. 17),
with two real wrinkles: Ps 115+116 joined under one shared antiphon on
Monday, and Ps 138/143 each split across two verse-ranges under one
shared antiphon (Thu/Fri) — modeled by letting a group's `psalms` list
take `{number, verses}` entries alongside plain numbers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 18:58:45 -04:00
will 1a1b296c5f Model the Paschaltide alleluia suffix on Common-category antiphons
Deploy / deploy (push) Successful in 56s
Live-querying settled the "sometimes 1, sometimes 2 allelúja" question:
categories without their own dedicated Paschaltide source-text chain
(Confessor-Bishop/Doctor, Abbot, Virgin, ...) get a mechanical single
suffix appended at render time, not a rank/octave-dependent count.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 14:19:32 -04:00
will 75b731221e Close out P/T/S/N: St. Benedict's/All Saints Benedictine's own content, St. Michael redirect bug
Deploy / deploy (push) Successful in 57s
The last 2 duplex-majus+ saints held back from the original 29-saint
P/T/S/N pass (for lacking a collect at the time) get the same
individual live-query treatment as their peers: real, unique proper
text for both (St. Benedict's own Prime antiphon is the opening line of
St. Gregory the Great's Life of St. Benedict; All Saints of the
Benedictine Order draws on Matt 13:43, Luke 6:22-23, and a well-known
All Saints antiphon) -- 14 new files, English translated throughout
since the source rendered none for either.

A final calendar-wide sweep (every saint's resolveMinorHourAntiphon/
resolveMinorHourChapter) turned up one more real gap, in the mechanism
itself rather than missing data: St. Michael's propers field points at
Apparition of St. Michael's own id (the two feasts are word-for-word
identical), and getDayCollect/getBenedictusAntiphon both explicitly
follow propers when resolving content -- but getMinorHourOverrideId
doesn't; it keys off the day's own winner id directly, so
resolveMinorHourAntiphon was looking for a file that could never exist.
Fixed the same way as Finding of St. Stephen: minorHoursCommon points
directly at his delegate's already-authored proper files.

Every real saint on this app's calendar now has real P/T/S/N content.
The only remaining absence is Ss. Ursula and Companions (no way to
verify a category of one) and the example-confessor test placeholder,
which was never meant to have any.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 11:34:21 -04:00
will 37dac8b476 Sweep minorHoursCommon (P/T/S/N) across the whole calendar, not just the 94-saint batch
Deploy / deploy (push) Successful in 56s
The original 29-saint P/T/S/N pass only ever covered duplex-majus+
saints; every below-majus saint from the *original*, pre-94-batch
109-saint pass had never had Prime/Terce/Sext/None attempted at all --
same gap already found and fixed for the 94-batch's own below-majus
saints, now closed for the rest of the calendar too.

Reused the Kalendaria-table-validated fileref approach rather than a
blind Sancti/MM-DD.txt read -- a real near-miss caught this early: St.
Apollonia's Sancti/02-09.txt is currently St. Cyril of Alexandria's (a
19th-century addition to that filename slot); blind reading would have
silently attributed his Doctor-of-the-Church Common to her. Also fixed
a regex bug the same pass turned up: the "vide"/"ex CXX" pattern
matched the bare word "Commune" (no number) and reading-specific
sub-references like "ex C4 in 2 loco" -- found via St. Clement, whose
Monastic-specific [Rule] override has neither a clean "vide CXX" nor a
number after "Commune."

90 below-majus saints swept, ~82 wired directly into the categories
already established in the previous commits -- no new content needed,
every C-number variant found already falls inside an already-verified
family.

A real bug this pass caught and fixed in its own output: Ss. Ursula and
Companions (deliberately left minorHoursCommon-unwired) got swept up
and wrongly wired, since she has collectCommon set despite propers:
null and being a genuine commemoration-only stub -- the Kalendaria
lookup silently defaulted to St. Hilarion's own primary slot at her
shared date. Caught by cross-checking every swept saint's propers field
for this exact pattern; reverted.

A real new category found: Doctors of the Church are not one thing.
6 of the 7 saints whose own [Rule] says "vide C4a" (Athanasius,
Augustine, Basil the Great, Hilary of Poitiers, John Chrysostom, Leo I)
render P/T/S/N byte-identical to the already-verified
common-of-a-confessor-bishop -- wired directly. St. Gregory the Great is
the one genuine exception: his own live-rendered P/T/S/N is real, unique
proper text (his own election, his Liber Regulae Pastoralis, the
dove-and-quill legend, the "non Angli sed Angeli" episode) -- 7 new
files, English translated throughout since the source rendered none.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 11:24:53 -04:00
will 130b0f983d Extend the source-file fallback to Frances of Rome/Bibiana, fix a real Agatha/Lucy gap
Deploy / deploy (push) Successful in 56s
Follow-up to the previous commit: applies the same "read the fileref
directly, no live query required" fix to St. Frances of Rome and St.
Bibiana, whose own filerefs simply hadn't been derived yet (they predate
the Kalendaria-table work this session built). Both resolve to
common-of-a-virgin, same as several of the 15 fixed previously.

Also swept every saint on the calendar (not just this batch's own
lineage) for any other still-missing collect or Benedictus antiphon,
which turned up a real, older gap: St. Agatha and St. Lucy's own file
comments already documented "resolves honestly missing... despite
propers being set" -- written before collectCommon existed as a
mechanism at all. Agatha's own [Oratio] is a name-substituted reference
confirmed identical to the already-existing collect-c6.yml template, so
collectCommon was simply wired in. Lucy's own [Oratio] references a
different Common formula unedited, with her own [Name] section supplying
the whole descriptive phrase a plain name field would otherwise need the
template for -- written as her own st-lucy-collect.yml rather than a new
single-use template, since she's the only current user of that unedited
form.

St. Clare showed up in the same sweep but is a false positive: her real
content lives in a separate commemoration-bundle file, used via a
different code path the sweep's synthetic-winner methodology doesn't
exercise -- already fully authored, no gap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 10:56:10 -04:00
will ea15e909df Resolve 15 "unwinnable" stubs' Benedictus antiphons from source directly
Deploy / deploy (push) Successful in 55s
Direct feedback on the previous commit's framing: "unwinnable" was
describing an artifact of Divinum Officium's own single-winner
precedence model, not a real content gap -- this app already renders
commemorated saints on their own terms and doesn't need a saint to win
a live query to have real content. Fixed by reading each of the 15
saints' own already-validated fileref directly (the exact same fileref
their collect was already sourced from during the Kalendaria-table
pull) for its [Ant 1] section, falling back to whatever Common its own
[Rule]/[Rank] "vide"/"ex CXX" reference names when there isn't one --
the same proper-then-Common shape getDayCollect/collectCommon already
uses, just applied to the Benedictus antiphon too.

14 of the 15 had no [Ant 1] of their own at all -- clean Common
fallback, and every one of their C-numbers falls inside a family
already live-query-verified in the previous commit, so
minorHoursCommon/benedictusCommon were wired directly with full
confidence: St. Marcus, St. Felix (Presbyter), St. Hermes, St.
Saturninus, St. Prisca, St. Thecla, St. Catherine of Siena, Ss.
Machabees, Ss. Marius/Martha/Audifax/Abachum, Ss. Twelve Brothers, Ss.
Alexander/Eventius/Theodulus/Juvenal, Ss. Felix/Simplicius/
Faustinus/Beatrix, Ss. Nabor and Felix. St. Felicitas needed one new
category (C7), but it turned out to be a cross-reference resolving to
text byte-identical to the already-verified common-of-a-virgin.

The 1 remaining (Ss. Euphemia, Lucy, and Geminian) does have her own
real [Ant 1] -- but marked draft, not verified, on principle: St.
Alexius's own file also has a genuine [Ant 1] that his actual
live-rendered Benedictus antiphon doesn't use (common-of-an-abbot's
text instead), so a raw [Ant 1] read alone isn't sufficient proof, and
for a saint that can never be live-checked at all that uncertainty
can't be resolved either way -- used the text anyway, honestly flagged
rather than claimed as confirmed.

St. Frances of Rome and St. Bibiana (the older, pre-Kalendaria-table
109-saint pass) are good candidates for this same fix but weren't
touched here, since their own fileref was never derived by this
session's work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 09:38:12 -04:00
will 9faa7168bc Author Benedictus antiphons for the remaining 26 propers:null saints
Deploy / deploy (push) Successful in 57s
Completes the 94-saint batch's Benedictus-antiphon coverage. A saint
with a genuine proper collect doesn't imply a genuine proper antiphon,
same finding as St. Martin of Tours's mixed proper/shared split from
the original 29-saint pass: of the 26, 15 are genuinely unwinnable
stubs (already documented from the collects pass -- always
commemorated, never the day's own title, so nothing to live-query) and
left honestly missing, matching St. Frances of Rome/St. Bibiana's
precedent from the original 109-saint pass; 7 of the remaining 11
turned out to render byte-identical to an already-authored Common
category (benedictusCommon wired directly, no new content) -- including
Ss. Dionysius and Companions, whose own `common` field says
common-of-a-martyr-bishop but whose live-verified minor-hour text
actually matches common-of-several-martyrs, another case (like Chair of
St. Peter earlier) where the two fields genuinely diverge. The other 4
have real unique text of their own: All Saints of the Benedictine
Order, St. Agnes (secunda), St. Benedict (no English in the source,
translated and marked draft, same gap as St. Scholastica's), and the
Vigil of St. John the Baptist (deliberately echoing the Benedictus
canticle's own opening words).

P/T/S/N for these 26 not attempted here -- a further layer on top, same
as how the original 109 got Benedictus antiphons long before their own
later, separate P/T/S/N pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 09:14:05 -04:00
will ffaa797940 Author Benedictus antiphon + P/T/S/N Commons for 66 propers:null saints
Deploy / deploy (push) Successful in 56s
Continues the 94-saint batch this collects pass unblocked. Raw
Commune/*.txt fields don't reliably match what the engine actually
renders per hour -- same lesson as the collects pass, confirmed again:
no CommuneM/*.txt file defines explicit per-hour antiphon fields for any
category this batch needed, and the already-authored
common-of-a-confessor-bishop content (from the original 29-saint pass)
doesn't match Commune/C4.txt's own raw [Ant 1] either, since it was
live-query-sourced rather than read from the file directly. Fixed by
querying web/cgi-bin/horas/officium.pl directly (local Perl, not a
network fetch) for one representative saint per Common category, with
every representative's title verified against an accent-insensitive
name-stem match before its content is trusted -- caught a stale-date bug
this way: St. Emerentiana's stored clean-year comment actually rendered
St. Hilary of Poitiers's office that year.

Cross-querying multiple saints per nominal SaintRecord.common category
found the real liturgical groupings are coarser (Martyr-Bishop and
Martyr; Abbot, Confessor-not-Bishop, and Confessor; Virgin-Martyr,
Virgin, and Holy Woman all render identical minor-hour text) but not
uniformly so (Pope-Martyrs render distinct allelúja-toned text from
plain Several-Martyrs; the Dedication of the Basilicas of Ss. Peter and
Paul draws from the Dedication Common, not its nominal
common-of-an-apostle at all). Net: 7 new shared Common categories plus
the pre-existing common-of-a-confessor-bishop (which also needed a new
Benedictus-antiphon file of its own) cover 66 of the 94 saints.

New mechanism: SaintRecord.benedictusCommon, with getBenedictusAntiphon
falling back to it the same way getDayCollect already falls back to
collectCommon -- no name-substitution needed here, since every Common
category in current use renders name-free Benedictus-antiphon text.

Also fixes a real bug this surfaced: getMinorHourOverrideId gated all
Prime/Terce/Sext/None lookup behind the same duplex-majus+ threshold
Lauds' own psalmody override uses, on the assumption that only a strong
feast carries its own minor-hour content -- live-querying proved that
wrong (a winning Semiduplex saint still renders its own antiphons, not
the ferial default). Lauds' psalmody override itself stays majus+-gated
correctly, since it's a much bigger per-saint commitment this batch
doesn't add a Common fallback for.

Ss. Ursula and Companions deliberately left without a minor-hour Common:
her collect template has no other calendar member to verify a shared
text against, and she can never be queried directly (a genuinely
unwinnable stub) -- left honestly unauthored rather than guessed.

The other 26 of the 94 (own proper collect, from the collects batch)
still need their own individually-authored Benedictus antiphon, same
shape as the original 109-saint pass -- not done in this pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 08:58:13 -04:00
will 6bb4c6a80f Author day collects for all 94 propers:null saints via Kalendaria tables
Deploy / deploy (push) Successful in 59s
Blind per-date source-file reading (Sancti/SanctiM MM-DD.txt) turned out
unreliable for collect authorship: those files are era-merged, carrying
centuries of accumulated saints behind rubric-conditional gating, so a
naive read often returned a saint canonized long after 1617 instead of
the Monastic-Tridentine-era one actually on this calendar (St. Didacus
instead of All Saints of the Benedictine Order, St. Rose of Lima instead
of Ss. Felix and Adauctus, etc).

Fixed by pulling from web/www/Tabulae/Kalendaria/'s own era-specific
calendar tables (1570.txt Tridentine base + M1617.txt's Monastic diffs)
instead, cross-validated against the pre-1955/Divino Afflatu chain
(1888->1906->1939->1954) as a corroborating signal. Cross-referencing all
205 existing saint records against this table confirmed 204/205 were
already correctly placed -- the earlier live-query verification work was
sound, only blind file-reading was flawed -- so no sanctoral-calendar.yml
corrections were needed, just the collect content itself.

Two collect shapes came out of the 94: 34 proper collects (own YAML file,
Latin verified against source, English translated+draft where the source
lacked it) and 60 resolved via a new Common-collect templating mechanism
-- SaintRecord.collectCommon/collectName plus substituteName() in
resolve-common.ts, filling a template's {N} placeholders positionally
from the saint's own declined name, across 21 new common/collect-c*.yml
templates named after the source's own C-numbering.

St. Michael's Sep 29 propers resolved via its whole-file @Sancti/05-08
inheritance (Apparition of St. Michael, May 8). St. Martha's plural-
template default was overridden to the singular collect-c6a since she's
one person, not a pair.

Also fixes a latent bug this surfaced: getDayCollect looked up
`${collectCommon}-collect`, but collect-c*.yml files are id'd as
`collect-c2` etc with no such suffix -- caught by a smoke test where
every one of the 93 non-Michael saints resolved as missing.

4 pre-existing tests updated: they asserted specific saints (St.
Benedict, St. Romanus Abbot, St. Anthony Abbot) had no authored collect,
which is no longer true; resolve-common.test.ts's generic "missing" case
now uses a synthetic day/winner since no real unauthored saint remains
as an example.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 08:19:28 -04:00
will d6ea898e3f Author Lauds psalmody+office-bundle for all 28 remaining duplex-majus+ saints
Deploy / deploy (push) Successful in 56s
Completes the Lauds content backlog: every saint with Prime/Terce/Sext/
None content already authored this session now also has its own Lauds
psalmody override (3 psalm antiphons + Canticle of the Three Young Men +
Laudate antiphon) and office bundle (capitulum/responsory/hymn/versicle),
live-verified against Divinum Officium (Monastic Tridentinum 1617).
Combined with the 4 already done (st-lawrence, marian-saturday,
christ-the-king, christmas-octave-sunday), the psalmody-override and
office-bundle backlogs are both now fully closed for every non-`propers:
null` duplex-majus+ id.

Confirmed structurally, not assumed: the 5 Lauds antiphons are the same
pool Prime/Terce/Sext/None draw from (Prime=1, Terce=2, Sext=3, None=5,
skipping the canticle's own 4th).

St. Scholastica again needed self-translation (no English exists in the
source for her Lauds content either) -- four of five antiphons and her
capitulum reuse the English already verified for her Terce/Sext/None
content; the canticle antiphon, versicle, and 6-stanza hymn are new
translations. Surfaced a real schema gap along the way: the Lauds
psalmody-override type has no per-language status field the way the
Prime/Terce/Sext/None mechanism does, so her draft English can't carry
the same "unverified" UI marker there -- flagged in TODO.md, not fixed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 19:19:16 -04:00
will 6bab6047c1 Author full Christmas-Octave Sunday content, fix a temporal-id override gap
Deploy / deploy (push) Successful in 57s
christmas-octave-sunday previously had only its Benedictus antiphon
authored -- no Lauds psalmody override or office bundle, no Prime/Terce/
Sext/None content at all. Adds the full set: Lauds psalmody (3 psalms +
Canticle of the Three Young Men + Laudate, all distinct antiphons),
Lauds capitulum/versicle/hymn ("A solis ortus cardine"), and Prime/Terce/
Sext/None antiphons+chapters (Terce/Sext/None only, matching Gal
4:1-2/4:4-5/4:7 -- the same reading the raw Monastic Tridentinum 1617
source file gives for this Sunday).

Real code gap found and fixed along the way: getMinorHourOverrideId
(hours/resolve-common.ts) only checked the sanctoral branch of
resolveOfficeWinner's result, so a named temporal feast in
ALWAYS_OVERRIDE_TEMPORAL_IDS (marian-saturday, christ-the-king) got a
Lauds psalmody override but silently never a Prime/Terce/Sext/None one,
however much was authored for it. Fixed by mirroring
getPsalmodyOverrideFor's own temporal-id branch (hours/lauds.ts), and
christmas-octave-sunday added to that set.

Content sourced from a "Divino Afflatu 1954" live query rather than
Monastic Tridentinum 1617, since 1617 has no real identity of its own for
this Sunday under its own calendar rules on most Dec 26-29 dates (only
the collect, already authored separately, happened to be pulled from a
date where 1617 did render it) -- consistent with this app's stated
precedent of following D.A. for calendar/precedence even while keeping
1617's shape for the hours themselves elsewhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 16:16:33 -04:00
will bcae61568a Add Paschaltide-variant fallback for minor-hour Commons
Deploy / deploy (push) Failing after 57s
Confirms and formalizes what TODO.md had flagged as an open question:
the reference engine's own source has explicit Paschaltide variants
(Commune/C1p.txt, C2p.txt, C3p.txt -- "tempore Paschali") for exactly 3
of its 12 Common categories -- Apostles, a Martyr-Bishop, Several
Martyrs -- a real liturgical restriction of Alleluia/victory imagery to
apostles and martyrs, not a coincidence.

resolveMinorHourAntiphon/resolveMinorHourChapter now try a
`${minorHoursCommon}-paschaltide` variant first whenever day.season is
eastertide/ascensiontide/pentecost (the same three-season window as
Compline's own Regina Caeli table, since "tempore paschali" runs through
the Pentecost octave in both), falling back to the plain Common when no
variant is authored.

common-of-apostles-paschaltide (7 files) ported directly from the
reference engine's own Commune/C1p.txt source files rather than
live-queried, then cross-checked against st-mark's already-authored,
live-queried content -- identical except one antiphon's asterisk, which
the source places one word earlier; corrected to match. st-mark and
st-john-before-the-latin-gate's duplicate per-saint files removed
entirely; ss-philip-and-james's chapter files removed (its antiphons
stay proper).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 16:03:06 -04:00
3926 changed files with 294766 additions and 2029 deletions
+4
View File
@@ -2,3 +2,7 @@ node_modules
dist dist
dev-dist dev-dist
*.local *.local
CLAUDE.md
MEMORY.md
.claude/
+72
View File
@@ -0,0 +1,72 @@
# vu
A static, offline-first PWA for praying the Divine Office — Matins, Lauds, Prime, Terce, Sext,
None, Vespers, and Compline — in bilingual Latin/English, for any given date.
This isn't a reconstruction of any single historical breviary. It's a personal design that
draws on several historical sources without reproducing any one of them exactly: hour
structure chiefly from the Monastic Tridentinum (1617), precedence/rubrics from Divino
Afflatu, and a calendar merged from both the Tridentine 1906 and Divino Afflatu calendars,
with more generous handling of commemorations and octaves than any one historical rite used.
See `CLAUDE.md` (gitignored, not shipped in this repo) or the project's own notes for the full
rationale if you're extending it.
There's no backend and no database — everything is a pre-built static bundle, and the app is
designed to keep working fully offline once it's been loaded once.
## Development
Requires Node.js (developed against Node 20+).
```sh
npm install
npm run dev # start the Vite dev server
npm run build # type-check (tsc --noEmit) and produce a production build in dist/
npm run preview # serve the production build locally
npm test # run the test suite (vitest)
```
Run a single test file or a subset by name:
```sh
npx vitest run tests/hours/vespers.test.ts
npx vitest run -t "some test name"
```
## Running in production
The app is entirely static once built, so "running it" just means serving `dist/` — any static
file host works, as long as it falls back to `index.html` for unknown paths (the app does its
own client-side routing for URLs like `/2026-08-09/prime`).
### Docker
A `Dockerfile` builds the static bundle with Node and serves it with Caddy:
```sh
docker build -t vu .
docker run -p 8080:80 vu
```
Or with Docker Compose (`docker-compose.yml` is included):
```sh
docker compose up -d --build
```
This serves the app on `http://localhost:8080`. Put a reverse proxy/TLS terminator in front of
it for a real deployment; the container itself just listens on port 80 internally and has no
configuration, secrets, or external dependencies.
### Without Docker
Any static file server works — build the app, then serve `dist/` with SPA-style fallback
routing. For example, with Caddy directly:
```sh
npm run build
caddy file-server --root dist --listen :8080
```
(For a real deployment you'll want a fallback rule so unknown paths resolve to `index.html`
see the `Caddyfile` in this repo for the pattern.)
+5907 -110
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
# Generic production compose file — builds the static bundle and serves it
# with Caddy on port 8080. Not tied to any particular host's network/proxy
# setup; put a reverse proxy or TLS terminator in front of it as needed.
services:
vu:
build: .
container_name: vu
restart: unless-stopped
ports:
- "8080:80"
+39 -5
View File
@@ -16,9 +16,22 @@ export function todayIso(): string {
return `${yyyy}-${mm}-${dd}`; return `${yyyy}-${mm}-${dd}`;
} }
/** Which hour to default to when the URL doesn't specify one, based on local wall-clock time. */
export function defaultHourForTime(date: Date): HourId {
const h = date.getHours();
if (h < 6) return 'matins';
if (h < 8) return 'lauds';
if (h < 9) return 'prime';
if (h < 12) return 'terce';
if (h < 15) return 'sext';
if (h < 17) return 'none';
if (h < 21) return 'vespers';
return 'compline';
}
const state: AppState = { const state: AppState = {
date: todayIso(), date: todayIso(),
selectedHour: 'prime', selectedHour: defaultHourForTime(new Date()),
languages: ['en'], languages: ['en'],
}; };
@@ -43,14 +56,35 @@ export function setDate(date: string): void {
notify(); 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 { 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 {
const d = new Date(`${date}T00:00:00Z`);
d.setUTCDate(d.getUTCDate() + days);
return d.toISOString().slice(0, 10);
} }
export function shiftDate(days: number): void { export function shiftDate(days: number): void {
const d = new Date(`${state.date}T00:00:00Z`); setDate(addDays(state.date, days));
d.setUTCDate(d.getUTCDate() + days);
setDate(d.toISOString().slice(0, 10));
} }
export function setSelectedHour(hourId: HourId): void { export function setSelectedHour(hourId: HourId): void {
+2 -2
View File
@@ -19,11 +19,11 @@ export interface CollisionResult {
} }
function asWinner(candidate: SanctoralIdentity): DayWinner { 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 { 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 };
} }
/** /**
+78 -16
View File
@@ -38,6 +38,34 @@ export function isAtLeast(rank: FeastClass, threshold: FeastClass): boolean {
return compareFeastClass(rank, threshold) >= 0; return compareFeastClass(rank, threshold) >= 0;
} }
/**
* The minimum rank a real sanctoral candidate needs to win each temporal
* category outright, per `decideOccurrence`'s own branches above —
* factored out so calendar/octaves.ts can ask the identical question of an
* *octave*'s own effective standing (not a real occurring saint) on a day
* where nothing else won. `undefined` where nothing ever wins outright no
* matter how high-ranked (`privileged-feria-major`/`privileged-sunday` —
* Ash Wednesday, Holy Week, a privileged Sunday — only ever get a
* commemoration at best, per those branches above). `ordinary-feria` has
* no real threshold at all (any real content wins), included here as
* `'simplex'` (the weakest real rank) only for completeness — callers
* needing that case already have their own unconditional path and don't
* need to call this. */
export function minimumOutrightWinningRank(category: TemporalCategory): FeastClass | undefined {
switch (category) {
case 'ordinary-feria':
return 'simplex';
case 'privileged-feria-minor':
return 'semiduplex';
case 'privileged-feria':
case 'ordinary-sunday':
return 'duplex';
case 'privileged-feria-major':
case 'privileged-sunday':
return undefined;
}
}
/** Vigils belong to the day *before* their feast, so an impeded vigil is /** Vigils belong to the day *before* their feast, so an impeded vigil is
* shifted backward rather than forward like everything else — see * shifted backward rather than forward like everything else — see
* calendar/transfer.ts. */ * calendar/transfer.ts. */
@@ -55,11 +83,11 @@ export interface OccurrenceResult {
} }
function sanctoralCommemoration(candidate: SanctoralIdentity): Commemoration { 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 { 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 };
} }
/** /**
@@ -115,11 +143,22 @@ export function decideOccurrence(
}; };
case 'privileged-feria-major': case 'privileged-feria-major':
// Ash Wednesday, Holy Week, the Easter Octave, the Vigil of // Ash Wednesday, Holy Week, the Easter Octave, Low Sunday, the
// Christmas — never displaced, same as privileged-sunday. // Vigils of Christmas/Pentecost — never displaced, and never gives
if (isAtLeast(sanctoral.rank, 'duplex-majus')) { // even a bare commemoration to any rank, same as privileged-sunday.
return { winner: temporalWinner, commemorations: [sanctoralCommemoration(sanctoral)] }; // 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 { return {
winner: temporalWinner, winner: temporalWinner,
commemorations: [], commemorations: [],
@@ -137,8 +176,22 @@ export function decideOccurrence(
return { winner: temporalWinner, commemorations: [sanctoralCommemoration(sanctoral)] }; return { winner: temporalWinner, commemorations: [sanctoralCommemoration(sanctoral)] };
} }
// Semiduplex or Vigil: no room here at all, in either direction — // Semiduplex or Vigil: no room here at all, in either direction —
// better to preserve the feast whole on another day than downgrade // per direct instruction, an ordinary Sunday's own standing pushes
// it to a bare commemoration. // a Semiduplex feast off to the next open day (typically the
// Monday right after) rather than downgrading it to a bare
// commemoration. This `transfer` signal is only half of the
// mechanism: it's just this day noting "I couldn't hold onto this
// candidate." The other half — a later day actually claiming a
// transferred-in candidate — lives entirely in calendar/index.ts's
// resolveDay: for *every* date it resolves, it independently
// recomputes `resolveNativeOccurrence` for both the day before and
// the day after, checks whether *that* neighbor's own native
// occurrence produced a `transfer` pointed this direction, and only
// then folds it in via `applyIncomingTransfer`. So "Monday looks
// back at the sanctoral calendar to see if Sunday's own candidate
// needs a home" isn't a separate mechanism to build — it's the
// existing generic transfer-consumption check, which already runs
// for every day regardless of category.
return { return {
winner: temporalWinner, winner: temporalWinner,
commemorations: [], commemorations: [],
@@ -146,13 +199,22 @@ export function decideOccurrence(
}; };
case 'privileged-sunday': case 'privileged-sunday':
if (isAtLeast(sanctoral.rank, 'duplex-majus')) { // A privileged Sunday wants nothing at all, no matter how high the
// Never displaced, but a sufficiently high feast still gets a nod. // candidate ranks — not even the bare commemoration an ordinary
return { winner: temporalWinner, commemorations: [sanctoralCommemoration(sanctoral)] }; // Sunday would allow a Simplex, and no exception for duplex-majus+ —
} // everything transfers. Live-verified 2026-09-01, three independent
// Below duplex-majus, a privileged Sunday wants nothing at all — // dates: the Immaculate Conception (Duplex I. classis) transfers off
// not even the bare commemoration an ordinary Sunday would allow a // Advent II with no commemoration at all (real dates 2024/2030/2041),
// Simplex — so everything here transfers. // 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 { return {
winner: temporalWinner, winner: temporalWinner,
commemorations: [], commemorations: [],
+565 -59
View File
@@ -9,12 +9,78 @@
// become a configurable choice later (the same day->id indirection // become a configurable choice later (the same day->id indirection
// philosophy already used for the sanctoral calendar), not hardcoded here // philosophy already used for the sanctoral calendar), not hardcoded here
// forever — just not built yet. // forever — just not built yet.
import type { LiturgicalDay } from './types'; //
// 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) 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 { easterSunday } from './easter';
import { adventStart, firstSundayStrictlyAfter, sundayOnOrBefore } from './temporal'; import { adventStart, firstSundayStrictlyAfter, sundayOnOrBefore } from './temporal';
import { addDays, daysBetween, toIsoDate } from './date-math'; import { addDays, daysBetween, toIsoDate } from './date-math';
import { getTemporalFeastRecord } from './temporal-feasts'; import { getTemporalFeastRecord } from './temporal-feasts';
import { resolveActiveOctave, type ActiveOctave } from './octaves'; import { activeOctavesFor, octaveGoverningPrivilegedDay, resolveActiveOctave, type ActiveOctave } from './octaves';
import { resolveTemporalId } from './temporal-id';
interface Bi {
en: string;
la: string;
[lang: string]: string;
}
function bi(en: string, la: string): Bi {
return { en, la };
}
/** 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'),
};
export function emberDayLabel(id: string): Bi | undefined {
return EMBER_DAY_NAMES[id];
}
function capitalize(text: string): string { function capitalize(text: string): string {
return text.charAt(0).toUpperCase() + text.slice(1); return text.charAt(0).toUpperCase() + text.slice(1);
@@ -37,6 +103,56 @@ function ordinal(n: number): string {
} }
} }
// Feminine Latin ordinal adjectives (agreeing with "Dominica"/"Hebdomada"/
// "die", all feminine) — covers 1st through 24th, the full range this app
// ever counts to (Trinitytide's fixed last week is the 23rd). Not a general
// Latin-numerals utility, just this app's own closed display vocabulary.
const ORDINAL_LA = [
'',
'prima',
'secunda',
'tertia',
'quarta',
'quinta',
'sexta',
'septima',
'octava',
'nona',
'decima',
'undecima',
'duodecima',
'decima tertia',
'decima quarta',
'decima quinta',
'decima sexta',
'decima septima',
'decima octava',
'decima nona',
'vicesima',
'vicesima prima',
'vicesima secunda',
'vicesima tertia',
'vicesima quarta',
];
function ordinalLa(n: number): string {
return ORDINAL_LA[n] ?? `${n}a`;
}
const WEEKDAY_LABELS: Record<Weekday, Bi> = {
sunday: bi('Sunday', 'Dominica'),
monday: bi('Monday', 'Feria Secunda'),
tuesday: bi('Tuesday', 'Feria Tertia'),
wednesday: bi('Wednesday', 'Feria Quarta'),
thursday: bi('Thursday', 'Feria Quinta'),
friday: bi('Friday', 'Feria Sexta'),
saturday: bi('Saturday', 'Sabbato'),
};
function weekdayLabel(day: LiturgicalDay): Bi {
return WEEKDAY_LABELS[day.weekday];
}
interface OrdinalSeason { interface OrdinalSeason {
/** ISO date of the season's own anchor day, for a given calendar year. */ /** ISO date of the season's own anchor day, for a given calendar year. */
anchorDate(year: number): string; anchorDate(year: number): string;
@@ -44,48 +160,80 @@ interface OrdinalSeason {
* Lent: the anchor is its own named day, excluded from the count — the * Lent: the anchor is its own named day, excluded from the count — the
* numbered weeks start the following Sunday. */ * numbered weeks start the following Sunday. */
includeAnchorWeek: boolean; includeAnchorWeek: boolean;
/** Used in "Weekday after {anchorName}" for the anchor's own partial week. */ /** Used in "Weekday after {anchorName}" for the anchor's own partial week,
anchorName: string; * and as the anchor day's own standalone name. */
/** Used in "the Nth Sunday/week {preposition} {ordinalName}". */ anchorName: Bi;
ordinalName: string; /** Used in "the Nth Sunday/week {preposition} {ordinalName}". For
* `preposition: 'of'`, `ordinalName.la` is already the correct genitive
* form (glued directly after the ordinal, no linking word — Latin
* idiom). For `preposition: 'after'`, `ordinalName.la` is the form used
* after "post" (traditionally accusative, or an idiomatic fixed phrase
* like "post Cineres"). */
ordinalName: Bi;
preposition: 'after' | 'of'; preposition: 'after' | 'of';
/** The season's own anchor day's rank, if it sits on the closed
* pre-1955 duplex scale at all — Ash Wednesday (Lent's anchor) doesn't;
* a privileged feria has no duplex-scale rank, so it's left undefined
* and shows no parenthetical, same "omit rather than guess" convention
* `formatRank`/`status` fields use elsewhere. */
anchorRank?: FeastClass;
/** Advent and Lent only: once the header always shows the day's weekday
* on its own, repeating it at the front of this label is redundant — so
* those two seasons drop it here, in both languages. Every other season
* keeps the weekday prefix (explicit, narrower scope decision). */
dropWeekdayPrefix?: boolean;
} }
const ORDINAL_SEASONS: Partial<Record<string, OrdinalSeason>> = { const ORDINAL_SEASONS: Partial<Record<string, OrdinalSeason>> = {
advent: { advent: {
anchorDate: adventStart, anchorDate: adventStart,
includeAnchorWeek: true, includeAnchorWeek: true,
anchorName: 'Advent', anchorName: bi('Advent', 'Adventus'),
ordinalName: 'Advent', // Genitive of 4th-declension "adventus" is also "Adventus" — no
// linking word needed: "Hebdomada II Adventus".
ordinalName: bi('Advent', 'Adventus'),
preposition: 'of', preposition: 'of',
dropWeekdayPrefix: true,
}, },
epiphanytide: { epiphanytide: {
anchorDate: (year) => `${year}-01-06`, anchorDate: (year) => `${year}-01-06`,
includeAnchorWeek: false, includeAnchorWeek: false,
anchorName: 'Epiphany', anchorName: bi('Epiphany', 'Epiphania'),
ordinalName: 'Epiphany', // Accusative, for "post Epiphaniam".
ordinalName: bi('Epiphany', 'Epiphaniam'),
preposition: 'after', preposition: 'after',
anchorRank: 'duplex-1-classis',
}, },
lent: { lent: {
anchorDate: (year) => addDays(toIsoDate(easterSunday(year)), -46), anchorDate: (year) => addDays(toIsoDate(easterSunday(year)), -46),
includeAnchorWeek: false, includeAnchorWeek: false,
anchorName: 'Ash Wednesday', anchorName: bi('Ash Wednesday', 'Feria Quarta Cinerum'),
ordinalName: 'Lent', // Genitive of 1st-declension "Quadragesima": "Hebdomada II
// Quadragesimae". The pre-Lent-I days use the real traditional idiom
// "post Cineres" instead (see temporalLabel below), not this field.
ordinalName: bi('Lent', 'Quadragesimae'),
preposition: 'of', preposition: 'of',
dropWeekdayPrefix: true,
}, },
eastertide: { eastertide: {
anchorDate: (year) => toIsoDate(easterSunday(year)), anchorDate: (year) => toIsoDate(easterSunday(year)),
includeAnchorWeek: false, includeAnchorWeek: false,
anchorName: 'Easter', anchorName: bi('Easter', 'Pascha'),
ordinalName: 'Easter', // "Pascha" is indeclinable — same form after "post".
ordinalName: bi('Easter', 'Pascha'),
preposition: 'after', preposition: 'after',
anchorRank: 'duplex-1-classis',
}, },
trinitytide: { trinitytide: {
anchorDate: (year) => addDays(toIsoDate(easterSunday(year)), 56), anchorDate: (year) => addDays(toIsoDate(easterSunday(year)), 56),
includeAnchorWeek: false, includeAnchorWeek: false,
anchorName: 'Trinity Sunday', anchorName: bi('Trinity Sunday', 'Dominica Sanctissimae Trinitatis'),
ordinalName: 'Trinity', // Accusative, for "post Sanctissimam Trinitatem" — this app's own
// Trinity-counted convention (see file header), not the historical
// Pentecost-counted "post Pentecosten".
ordinalName: bi('Trinity', 'Sanctissimam Trinitatem'),
preposition: 'after', preposition: 'after',
anchorRank: 'duplex-1-classis',
}, },
}; };
@@ -97,28 +245,99 @@ const ORDINAL_SEASONS: Partial<Record<string, OrdinalSeason>> = {
* octave, but nobody calls it that). Advent's own anchor (Advent I Sunday) * octave, but nobody calls it that). Advent's own anchor (Advent I Sunday)
* doesn't take this branch — it's already "the 1st Sunday of Advent" via * doesn't take this branch — it's already "the 1st Sunday of Advent" via
* the ordinal path below. */ * the ordinal path below. */
function anchorDayName(day: LiturgicalDay): string | undefined { function anchorDayName(day: LiturgicalDay, withRank = true): Bi | undefined {
const config = ORDINAL_SEASONS[day.season]; const config = ORDINAL_SEASONS[day.season];
if (!config || config.includeAnchorWeek) { if (!config || config.includeAnchorWeek) {
return undefined; return undefined;
} }
const year = Number(day.date.slice(0, 4)); const year = Number(day.date.slice(0, 4));
if (day.date === config.anchorDate(year)) { if (day.date === config.anchorDate(year)) {
return config.anchorName; return withRank && config.anchorRank
? bi(`${config.anchorName.en} (${formatRank(config.anchorRank).en})`, `${config.anchorName.la} (${formatRank(config.anchorRank).la})`)
: config.anchorName;
} }
return undefined; return undefined;
} }
function temporalLabel(day: LiturgicalDay): string { /** The fixed final Sunday of the liturgical year (temporal-id.ts's
const weekdayName = capitalize(day.weekday); * `post-pentecost-24`) always carries "Sunday XXIV after Pentecost"'s own
* formulary, regardless of how many Sundays actually elapsed since
* Trinity — confirmed by scanning every year 1900-2100, every single one
* lands on this id for its own last Sunday before Advent, never a raw
* elapsed-week count. This app's Trinity-counted display is one week off
* from that Pentecost-counted id (Trinity Sunday itself = post-pentecost-
* 01 = the display's own uncounted anchor day), so the fixed display
* ordinal here is XXIV - 1 = 23, not whatever a plain "weeks since
* Trinity" calculation would produce. */
const FIXED_LAST_SUNDAY_ORDINAL = 23;
/**
* Trinitytide's ordinal display can't be pure "weeks since Trinity's own
* first Sunday" arithmetic once the season gets late enough — see
* temporal-id.ts's own post-Pentecost-XXIII-plus branch. Two cases, both
* driven by `resolveTemporalId` directly (the same mechanism the content
* layer already trusts, rather than re-deriving the wdist arithmetic here
* a second time, so the label can never disagree with what's actually
* rendered underneath):
* - A resumed post-Epiphany Sunday/week (an overflow year's skipped
* Epiphany Sundays, reappearing here) — reads "after Epiphany", not
* the next Trinity-counted number in line.
* - The fixed final Sunday/week of the year itself — always the fixed
* 23rd-after-Trinity ordinal (see FIXED_LAST_SUNDAY_ORDINAL above),
* every year, not just overflow ones.
* Only ever fires within trinitytide, since that's the only season
* resolveTemporalId can return either of these ids for a date outside
* their own native season. Trinitytide isn't in the weekday-drop set, so
* both languages keep their weekday prefix here.
*/
function trinitytideOverrideLabel(day: LiturgicalDay): Bi | undefined {
if (day.season !== 'trinitytide') {
return undefined;
}
const id = resolveTemporalId(day.date);
const weekdayName = weekdayLabel(day);
const epiphanyMatch = id.match(/^post-epiphany-(\d)$/);
if (epiphanyMatch) {
const n = Number(epiphanyMatch[1]);
return day.weekday === 'sunday'
? bi(`The ${ordinal(n)} Sunday after Epiphany`, `Dominica ${ordinalLa(n)} post Epiphaniam`)
: bi(`${weekdayName.en} in the ${ordinal(n)} week after Epiphany`, `${weekdayName.la} in hebdomada ${ordinalLa(n)} post Epiphaniam`);
}
if (id === 'post-pentecost-24') {
return day.weekday === 'sunday'
? bi(
`The ${ordinal(FIXED_LAST_SUNDAY_ORDINAL)} Sunday after Trinity`,
`Dominica ${ordinalLa(FIXED_LAST_SUNDAY_ORDINAL)} post Sanctissimam Trinitatem`,
)
: bi(
`${weekdayName.en} in the ${ordinal(FIXED_LAST_SUNDAY_ORDINAL)} week after Trinity`,
`${weekdayName.la} in hebdomada ${ordinalLa(FIXED_LAST_SUNDAY_ORDINAL)} post Sanctissimam Trinitatem`,
);
}
return undefined;
}
function temporalLabel(day: LiturgicalDay): Bi {
const trinitytideOverride = trinitytideOverrideLabel(day);
if (trinitytideOverride) {
return trinitytideOverride;
}
const weekdayName = weekdayLabel(day);
const config = ORDINAL_SEASONS[day.season]; const config = ORDINAL_SEASONS[day.season];
if (!config) { if (!config) {
// No ordinal convention modeled for this season (Septuagesima-tide, // No ordinal convention modeled for this season (Septuagesima-tide,
// Passiontide, Ascensiontide, Pentecost, Christmastide, the // Passiontide, Ascensiontide, Pentecost, Christmastide, the
// Corpus-Christi/Sacred-Heart single-day seasons) — their few days // Corpus-Christi/Sacred-Heart single-day seasons) — their few days
// mostly have their own proper names rather than ordinal counting, so // mostly have their own proper names rather than ordinal counting, so
// this fallback is expected to be seen, not a gap to fill later. // this fallback is expected to be seen, not a gap to fill later. No
return `${weekdayName} in ${capitalize(day.season.replace(/-/g, ' '))}`; // authored Latin season name exists for these yet, so `la` falls back
// to the same English season word.
const seasonName = capitalize(day.season.replace(/-/g, ' '));
return bi(`${weekdayName.en} in ${seasonName}`, `${weekdayName.la} in ${seasonName}`);
} }
const year = Number(day.date.slice(0, 4)); const year = Number(day.date.slice(0, 4));
@@ -130,85 +349,372 @@ function temporalLabel(day: LiturgicalDay): string {
const firstNumberedSunday = config.includeAnchorWeek ? anchor : firstSundayStrictlyAfter(anchor); const firstNumberedSunday = config.includeAnchorWeek ? anchor : firstSundayStrictlyAfter(anchor);
if (day.date < firstNumberedSunday) { if (day.date < firstNumberedSunday) {
return `${weekdayName} after ${config.anchorName}`; // Lent's own pre-Lent-I days use the real traditional idiom "post
// Cineres" rather than the season's genitive `ordinalName` (which is
// built for the "of Lent" construction, not "after Ash Wednesday").
const afterAnchorLa = day.season === 'lent' ? 'Cineres' : config.anchorName.la;
return config.dropWeekdayPrefix
? bi(`After ${config.anchorName.en}`, `Post ${afterAnchorLa}`)
: bi(`${weekdayName.en} after ${config.anchorName.en}`, `${weekdayName.la} post ${afterAnchorLa}`);
} }
const weeksSince = daysBetween(firstNumberedSunday, sundayOnOrBefore(day.date)) / 7; const weeksSince = daysBetween(firstNumberedSunday, sundayOnOrBefore(day.date)) / 7;
const ordinalStr = ordinal(weeksSince + 1); const ordinalStr = ordinal(weeksSince + 1);
const ordinalStrLa = ordinalLa(weeksSince + 1);
if (day.weekday === 'sunday') { if (day.weekday === 'sunday') {
return `The ${ordinalStr} Sunday ${config.preposition} ${config.ordinalName}`; return config.preposition === 'of'
? bi(`The ${ordinalStr} Sunday of ${config.ordinalName.en}`, `Dominica ${ordinalStrLa} ${config.ordinalName.la}`)
: bi(`The ${ordinalStr} Sunday after ${config.ordinalName.en}`, `Dominica ${ordinalStrLa} post ${config.ordinalName.la}`);
} }
return `${weekdayName} in the ${ordinalStr} week ${config.preposition} ${config.ordinalName}`; const weekPhraseLa =
config.preposition === 'of' ? `hebdomada ${ordinalStrLa} ${config.ordinalName.la}` : `hebdomada ${ordinalStrLa} post ${config.ordinalName.la}`;
return config.dropWeekdayPrefix
? bi(`In the ${ordinalStr} week ${config.preposition} ${config.ordinalName.en}`, `In ${weekPhraseLa}`)
: bi(`${weekdayName.en} in the ${ordinalStr} week ${config.preposition} ${config.ordinalName.en}`, `${weekdayName.la} in ${weekPhraseLa}`);
} }
/** "Third Day within the Octave of St. Lawrence" — the real DO title an /** "Third Day within the Octave of St. Lawrence" for an ordinary mid-octave
* octave day carries on its own (e.g. "Tertia die infra Octavam S. * day ("Tertia die infra Octavam S. Laurentii Martyris"), or plain "Octave
* Laurentii Martyris") when nothing else has displaced it. Day 1 shouldn't * of St. Lawrence" for the octave's own closing day ("In Octava S.
* normally reach this (that day's own winner is the feast itself, handled * Laurentii Martyris") — real DO Latin distinguishes "infra octavam"
* above before this is ever called) — kept simple rather than * (within the octave, days 2-7) from "in octava" (on the octave day
* special-cased for that rare edge case (see applyOctaves's own * itself, day 8), live-verified: Aug 17 (St. Lawrence's closing day)
* `isOwnStartDay` comment in calendar/index.ts for when it can happen). */ * titles itself "In Octava S. Laurentii Martyris", not "Octava die infra
function octaveLabel(octave: ActiveOctave): string { * Octavam...". Day 1 shouldn't normally reach either branch (that day's
return `${ordinal(octave.dayNumber)} Day within the Octave of ${octave.name}`; * 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). 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 ${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 {
const core = octaveCoreName(octave);
const rank = formatRank(octave.wins);
return bi(`${core.en} (${rank.en})`, `${core.la} (${rank.la})`);
}
/** Same phrasing as `octaveLabel`, but without the parenthesized rank —
* the real DO commemoration line for a displaced octave day carries no
* rank at all (e.g. Aug 19's real Divino Afflatu 1954 commemoration reads
* plain "Quinta die infra Octavam S. Assumptionis Beatæ Mariæ Virginis",
* no "~ Semiduplex"). `octaveLabel`'s own rank parenthetical only belongs
* to an octave day when it's the day's own primary winner (rank is what
* it *won as*, not a fact worth restating once a duplex-or-higher saint
* has displaced it and it's just riding along as a commemoration
* instead). */
function octaveCommemorationLabel(octave: ActiveOctave): Bi {
return octaveCoreName(octave);
}
/** Real Divino Afflatu phrasing for a commemoration that only exists
* because Vespers/Compline crossed a day boundary — "commemoratio de
* præcedenti" (today, riding along on tomorrow's anticipated First
* Vespers) or "commemoratio de sequenti" (tomorrow, riding along on
* today's kept Second Vespers), per horascommon.pl. See
* `Commemoration`'s `vespersNote` field (calendar/types.ts) and
* calendar/vespers.ts's two `commemorationOfDisplaced*` functions, the
* only place that sets it. */
const CROSS_DAY_VESPERS_LABELS: Record<'today' | 'tomorrow', Bi> = {
today: bi('of today', 'de præcedenti'),
tomorrow: bi('of tomorrow', 'de sequenti'),
};
/** Every commemoration on `day`, resolved to display strings and grouped
* by kind — the single place that decides both "is this commemoration
* eligible to show at all here" and "how is it formatted", so every
* caller below shares the same answer instead of each hand-rolling its
* own filter/format pass (that duplication is exactly how Aug 19's octave
* phrasing, Aug 17's closing-day title, and Aug 16's missing Sunday
* commemoration ended up as three separate bugs instead of one). Callers
* still decide their own *order* — e.g. the octave-headline branch wants
* octaves before a commemorated saint, while a plain Sunday/feria wants a
* commemorated saint *before* its own temporal label — since that
* ordering reflects a real, deliberate liturgical convention per branch,
* not an accident to unify away.
*
* - `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). `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
* `resolveTemporalId(day.date)` specifically, not "any temporal-kind
* commemoration present", so unrelated side-notes like
* `applyEpiphany6Commemoration`'s fixed `post-epiphany-6` don't get
* mistaken for it. Rendered via `anchorDayName`/`temporalLabel`, the
* same machinery a primary temporal label uses, minus the rank.
* - `octave`: every active octave other than `excludeOctaveId` (the
* octave already serving as the day's own headline, if any) — only
* when `showOctaves` is true. Live-verified this is *not* simply "is an
* octave active": on a Sunday or privileged feria a saint won outright
* against, an unrelated active octave isn't commemorated at all (Aug
* 16's St. Joachim names only the Sunday, not St. Lawrence's/the
* Assumption's octaves, even though both are technically active) —
* octaves are only shown on `ordinary-feria` (no real standing of its
* own to prefer instead) or when the octave itself is governing the
* headline. Rendered via `octaveCommemorationLabel` — the "Nth Day
* within the Octave of X" phrasing, no rank.
*/
function collectCommemorations(
day: LiturgicalDay,
opts: { showOctaves: boolean; excludeOctaveId?: string },
): { sanctoral: Bi[]; temporal: Bi[]; octaves: Bi[] } {
const temporalId = resolveTemporalId(day.date);
const activeOctaves = opts.showOctaves ? activeOctavesFor(day.date) : [];
const sanctoral: Bi[] = [];
const temporal: Bi[] = [];
const octaves: Bi[] = [];
for (const c of day.commemorations) {
if (c.kind === 'sanctoral') {
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') {
// `c.id === temporalId` (the plain governing weekday, e.g. "Saturday
// in the 15th week after Trinity") is deliberately never shown here:
// this function only ever runs to list *secondary* identities
// alongside an already-displayed primary winner (a plain temporal
// day that wins outright never reaches `day.commemorations` at all —
// its own name is the header itself, via a different branch of
// `getDayLabel`). Once something else is already being celebrated,
// restating "this is also just the Nth ordinary weekday" tells the
// reader nothing they don't already know from the winner+rank shown
// — unlike a *named* temporal identity (an Ember day, a vigil), which
// genuinely adds information. Not Ember-specific (a 2026-09-05 first
// pass wrongly scoped this to "only when an Ember commemoration is
// also present" — user, 2026-09-05: "it has nothing to do with
// ember-ness"); this plain generic label is dropped unconditionally
// whenever it shows up as a mere commemoration, Ember day or not.
if (c.id !== temporalId) {
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) : biName(c.name, c.nameLa));
}
}
return { sanctoral, temporal, octaves };
}
const RANK_LABELS: Record<FeastClass, Bi> = {
simplex: bi('Simplex', 'Simplex'),
vigil: bi('Vigil', 'Vigilia'),
semiduplex: bi('Semiduplex', 'Semiduplex'),
duplex: bi('Duplex', 'Duplex'),
'duplex-majus': bi('Duplex Majus', 'Duplex Majus'),
'duplex-2-classis': bi('Duplex II Class', 'Duplex II. Classis'),
'duplex-1-classis': bi('Duplex I Class', 'Duplex I. Classis'),
};
/** The day's own winning saint's rank, parenthesized after their name —
* rank was previously shown nowhere in this app's UI at all, for any
* saint. Only the day's own *winner* gets this treatment, not every
* commemoration, matching the "winner is primary, commemorations are
* secondary" distinction this file already draws throughout. */
function formatRank(rank: FeastClass): Bi {
return RANK_LABELS[rank];
}
/** Sundays/ferias aren't on the `FeastClass` duplex scale at all (that
* scale is for feasts), so this is a separate, plain-string lookup rather
* than another `formatRank` case — used for the final ordinal-temporal-
* label fallback (weekdays, Sundays with no named-feast/anchor-day/octave
* standing of their own). Pre-1955, an ordinary Sunday's own rank is
* Semiduplex regardless of whether it's additionally "privileged"
* (privilege is about resisting supersession, a property of
* `temporalCategory`, not a higher spot on the duplex scale) — so both
* Sunday categories share one label here. Both "Semiduplex" and "Feria"
* are already the real Latin words, so `en`/`la` coincide here too. */
const TEMPORAL_CATEGORY_RANK_LABELS: Record<TemporalCategory, Bi> = {
'ordinary-sunday': bi('Semiduplex', 'Semiduplex'),
'privileged-sunday': bi('Semiduplex', 'Semiduplex'),
'ordinary-feria': bi('Feria', 'Feria'),
'privileged-feria-minor': bi('Feria', 'Feria'),
'privileged-feria': bi('Feria', 'Feria'),
'privileged-feria-major': bi('Feria', 'Feria'),
};
/**
* Explicit "which Vespers is this" status, for the Vespers hour header —
* distinct from `getDayLabel`'s name/rank/commemoration text, which is
* 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`,
* 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, 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: a feast name when * The full "day being celebrated" label: the day's own primary identity
* calendar/commemorations.ts says the day has one, combined with (or * (a sanctoral winner, a named temporal feast, a season's own anchor day,
* replaced by) the ordinal temporal label depending on whether the feast * an octave governing the day, or the plain ordinal temporal label — in
* won outright or was merely commemorated. See the plan discussion this * that precedence order) plus whatever `collectCommemorations` finds
* came from for the three cases. * eligible to ride along with it. Each branch below only decides two
* things: what the primary label is, and what commemoration *groups* are
* eligible here and in what order — the actual filtering/formatting work
* is `collectCommemorations`'s alone, shared by every branch. Returns a
* `Partial<Record<string, string>>` keyed by language code (`en`/`la`),
* the same convention `hours/types.ts`'s `ResolvedText`/`ResolvedVerse`
* already use.
*/ */
export function getDayLabel(day: LiturgicalDay): string { export function getDayLabel(day: LiturgicalDay): Partial<Record<string, string>> {
if (day.winner.kind === 'sanctoral') { if (day.winner.kind === 'sanctoral') {
return day.winner.name; // Octaves only ride along here on `ordinary-feria` — a day with real
// standing of its own (a Sunday, a privileged feria) doesn't mention
// an unrelated active octave even though it's technically active
// (live-verified: Aug 16, 2026, St. Joachim wins outright on a Sunday
// that's also within both St. Lawrence's and the Assumption's octave
// windows, and Divino Afflatu 1954's own commemoration line names
// only the Sunday, no octave).
const { sanctoral, temporal, octaves } = collectCommemorations(day, {
showOctaves: day.temporalCategory === 'ordinary-feria',
});
const rank = formatRank(day.winner.rank);
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, ...) // A named temporal feast (Christmas, Pentecost, Marian Saturday, ...)
// shows its own name rather than the ordinal week label — same // shows its own name rather than the ordinal week label — same
// "winner displaces, doesn't combine" rule a sanctoral winner gets // "winner displaces, doesn't combine" rule a sanctoral winner gets
// above. Most temporal ids don't have a record at all (see // above. Most temporal ids don't have a record at all (see
// temporal-feasts.ts) and fall through to the ordinal label as before. // temporal-feasts.ts) and fall through further down.
const namedFeast = getTemporalFeastRecord(day.winner.id); const namedFeast = getTemporalFeastRecord(day.winner.id);
if (namedFeast) { if (namedFeast) {
return 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 withTransferNote(day, joinBi([primary, ...sanctoral]));
} }
const commemoratedSaint = day.commemorations.find((c) => c.kind === 'sanctoral');
// A season's own named anchor day (Trinity Sunday, Easter, Ash // A season's own named anchor day (Trinity Sunday, Easter, Ash
// Wednesday, Epiphany) outranks an active octave, same reasoning as // Wednesday, Epiphany) outranks an active octave, same reasoning as
// anchorDayName's own doc comment — checked before the octave case // anchorDayName's own doc comment — checked before the octave case
// below since Trinity Sunday, e.g., also happens to be day 8 of // below since Trinity Sunday, e.g., also happens to be day 8 of
// Pentecost's octave, and the anchor name is what actually governs. // Pentecost's octave, and the anchor name is what actually governs.
// 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); const anchorName = anchorDayName(day);
if (anchorName) { if (anchorName) {
return commemoratedSaint ? `${commemoratedSaint.name}${anchorName}` : anchorName; const { sanctoral } = collectCommemorations(day, { showOctaves: false });
return withTransferNote(day, joinBi([anchorName, ...sanctoral]));
} }
// An active octave (St. Lawrence's, ...) is this day's real primary // An active octave (St. Lawrence's, ...) is this day's real primary
// identity in the live engine, not a footnote — e.g. "Tertia die infra // identity in the live engine, not a footnote — e.g. "Tertia die infra
// Octavam S. Laurentii Martyris", not "Wednesday in the 11th week after // Octavam S. Laurentii Martyris", not "Wednesday in the 11th week after
// Trinity" — but *only* when the temporal day itself has no standing of // Trinity" — but *only* when the temporal day itself has no standing of
// its own (`ordinary-feria`), same gate as hours/resolve-common.ts's // its own (`ordinary-feria`), or a foreign octave's own effective rank
// is strong enough to clear a privileged day's real threshold anyway
// (`octaveGoverningPrivilegedDay` — the Immaculate Conception's own
// octave outright winning several of its days against Advent's
// privileged-feria-minor ferias, live-verified: Dec 9/10/12/14 at the
// octave's ordinary Semiduplex, Dec 15 at its own elevated Duplex majus
// closing day). Same gate as hours/resolve-common.ts's
// resolveOfficeWinner and for the same reason: live-verified // resolveOfficeWinner and for the same reason: live-verified
// counterexample is the Christmas Octave's own stack (Dec 30, e.g., // counterexample is the Christmas Octave's own stack (Dec 30, e.g.,
// `privileged-feria-minor`), where the real title stays the temporal // `privileged-feria-minor`), where the real title stays the temporal
// Sunday's own ("De Dominica Infra Octavam Nativitatis") with no octave // Sunday's own ("De Dominica Infra Octavam Nativitatis") with no octave
// name in it at all — this label agreeing with resolveOfficeWinner // name in it at all, regardless of rank — `octaveGoverningPrivilegedDay`
// about which one wins is what makes "the office is Lawrence's" and // excludes `christmastide` for exactly this reason (see its own doc
// "the label says Lawrence" consistent instead of two independent // comment) — this label agreeing with resolveOfficeWinner about which
// guesses that can disagree. When more than one octave is active at // one wins is what makes "the office is Lawrence's" and "the label says
// once (resolveActiveOctave), the highest-ranked wins the headline // Lawrence" consistent instead of two independent guesses that can
// (ties broken by whichever started more recently) — the others still // disagree. When more than one octave is active at once
// ride along as ordinary octave commemorations, just not separately // (resolveActiveOctave), the highest-ranked wins the headline (ties
// named here. // broken by whichever started more recently) — every other active
const activeOctave = day.temporalCategory === 'ordinary-feria' ? resolveActiveOctave(day.date) : undefined; // octave still gets named too, not dropped: live-verified real case,
// Aug 17 -- St. Lawrence's own elevated closing day wins the headline,
// but the Assumption's own day 3 (a real, distinct, simultaneously-
// active octave, not a duplicate of Lawrence's) still belongs in the
// label alongside St. Hyacinth's commemoration.
const activeOctave =
day.temporalCategory === 'ordinary-feria' ? resolveActiveOctave(day.date) : octaveGoverningPrivilegedDay(day);
if (activeOctave) { if (activeOctave) {
const { sanctoral, octaves } = collectCommemorations(day, { showOctaves: true, excludeOctaveId: activeOctave.id });
const primary = octaveLabel(activeOctave); const primary = octaveLabel(activeOctave);
return commemoratedSaint ? `${primary}${commemoratedSaint.name}` : primary; return withTransferNote(day, joinBi([primary, ...octaves, ...sanctoral]));
} }
const temporal = temporalLabel(day); const temporalRank = TEMPORAL_CATEGORY_RANK_LABELS[day.temporalCategory];
return commemoratedSaint ? `${commemoratedSaint.name}${temporal}` : temporal; 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 withTransferNote(day, joinBi([temporal, ...sanctoral]));
} }
+72
View File
@@ -0,0 +1,72 @@
// Ember days (Quatuor Temporum) are privileged ferias, not named feasts —
// structurally different from calendar/movable-feasts.ts's rank-contest
// table. The precedence side (whether an occurring saint outranks the
// feria) is already handled generically by resolveTemporalCategory
// (September Ember days get their own `privileged-feria-minor` entry
// there; Advent Ember days inherit Advent's own season default) feeding
// the ordinary decideOccurrence contest -- this file ONLY handles content
// identity: relabeling the day's own temporal id so an Ember day's real,
// distinct proper content (collect, Matins readings) gets found instead
// of inheriting whatever Sunday governs that week, the same way
// applyMarianSaturday/applyChristmasOctaveSunday relabel a feria's own id
// for the same reason.
import type { Commemoration, DayWinner } from './types';
import { septemberEmberDayOffset, adventEmberDayOffset } from './temporal';
import { activeOctavesFor } from './octaves';
const SEPTEMBER_EMBER_IDS: Record<number, string> = {
3: 'ember-september-wednesday',
5: 'ember-september-friday',
6: 'ember-september-saturday',
};
const ADVENT_EMBER_IDS: Record<number, string> = {
3: 'ember-advent-wednesday',
5: 'ember-advent-friday',
6: 'ember-advent-saturday',
};
function emberIdFor(isoDate: string): string | undefined {
const septemberOffset = septemberEmberDayOffset(isoDate);
if (septemberOffset !== undefined) {
return SEPTEMBER_EMBER_IDS[septemberOffset];
}
const adventOffset = adventEmberDayOffset(isoDate);
if (adventOffset !== undefined) {
return ADVENT_EMBER_IDS[adventOffset];
}
return undefined;
}
/**
* Run after applyOctaves, same gate that function's own doc comment and
* applyMarianSaturday both rely on: an active octave (the Nativity of the
* BVM's, Sep 8-15, can genuinely overlap a late-anchored September Ember
* week in some years) already has real standing of its own and shouldn't
* be silently relabeled out from under it.
*
* Two cases, mirroring how a "Commemoratio ad Laudes tantum" reads
* live against the reference engine: if the feria itself is what's
* winning (`winner.kind === 'temporal'`, i.e. resolveTemporalCategory's
* `privileged-feria-minor` threshold already beat whatever saint was
* there, or nothing was there at all), its id is swapped for the Ember
* day's own — real content gets found where it wouldn't otherwise be. If
* a saint won outright instead (strong enough to clear that threshold),
* the winner is left alone but the Ember day is still commemorated
* alongside it, so its own collect/readings still surface (this app
* doesn't model the "Laudes only" hour-scoping nuance — same simplification
* level as every other commemoration here, not a new gap).
*/
export function applyEmberDay(isoDate: string, winner: DayWinner, commemorations: Commemoration[]): DayWinner {
if (activeOctavesFor(isoDate).length > 0) {
return winner;
}
const emberId = emberIdFor(isoDate);
if (!emberId) {
return winner;
}
if (winner.kind === 'temporal') {
return { kind: 'temporal', id: emberId };
}
commemorations.push({ kind: 'temporal', id: emberId });
return winner;
}
+91 -1
View File
@@ -15,6 +15,9 @@ import sanctoralCalendarData from '../data/calendar/sanctoral-calendar.yml';
export interface SaintRecord { export interface SaintRecord {
id: string; id: string;
name: 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; rank: FeastClass;
common: string; common: string;
propers: string | null; propers: string | null;
@@ -33,9 +36,95 @@ export interface SaintRecord {
* minor-hour content (either fully proper, or not yet authored). * minor-hour content (either fully proper, or not yet authored).
*/ */
minorHoursCommon?: string; minorHoursCommon?: string;
/**
* Fallback id for the day collect (`getDayCollect`) when this saint has
* no proper collect of their own (`propers` may still be non-null —
* e.g. St. Agatha and St. Lucy both have a proper antiphon but a
* Common-sourced collect) — the *full* id of a `data/propers/common/
* collect-c*.yml` file (e.g. `collect-c2-1`, not `c2-1` — no suffix
* gets appended), a *template* with a literal `{N}` standing in for
* the saint's own name, substituted via `collectName` below.
* Deliberately more granular than `common` above: the reference
* source's own Commune files split e.g. "Common of a Martyr Bishop"
* into several differently-worded collect templates (its own `C2`,
* `C2-1`, `C2a-1`, `C2b`, `C2b-1` — no clean semantic label recovered
* for *why* each saint gets a particular one, so these ids trace
* straight back to the source's own naming, e.g. `collect-c2-1`,
* rather than inventing a name that might be wrong.
*/
collectCommon?: string;
/** The `{N}` substitution values for `collectCommon`'s template, in
* whatever grammatical case that specific template's own placeholder
* position needs — taken directly from the reference source's own
* per-saint `[Name]` section (Latin/English/Sancti or SanctiM
* `MM-DD.txt`), not re-declined by hand. Arrays, filled in
* left-to-right against the template's own `{N}` occurrences in
* document order — most saints need exactly one name, but several
* Common templates (e.g. `collect-c3`, two co-named Martyr-Bishops)
* have two or more `{N}` placeholders for a group commemorated
* together, one array entry per saint in the group. */
collectName?: { la: string[]; en: string[] };
/**
* Fallback id for Lauds' Benedictus antiphon (`getBenedictusAntiphon`)
* when this saint has no proper `${propers}-antiphon` of their own —
* `data/propers/common/benedictus-antiphon-${id}.yml`. Unlike
* `collectCommon`, no name-substitution template: live-verified every
* Common category's Benedictus antiphon in current use is name-free
* text (the source's own `Commune/C4a.txt`, Doctor, is the one
* exception — has a `{N}`-shaped "beáte N." — but no saint on this
* calendar draws from it yet, so no substitution mechanism was built
* for this field; revisit if that changes). Often, but not always, the
* same category as `minorHoursCommon` below (both were live-verified
* 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 /** See calendar/types.ts's OctaveConfig doc comment. Absent for the vast
* majority of saints — only ones with a real octave declare it. */ * majority of saints — only ones with a real octave declare it. */
octave?: OctaveConfig; octave?: OctaveConfig;
/** For a `rank: vigil` record only — the real id of the feast this is
* the eve of (e.g. `st-andrew`, not derivable from this record's own id
* by stripping a `vigil-of-` prefix: several vigils' ids don't match
* their feast's id exactly — `vigil-of-the-assumption` vs `assumption`,
* `vigil-of-st-james` vs `st-james-the-greater`). Lets
* calendar/vespers.ts's evening-anticipation merge recognize "today's
* Vigil is being displaced by the very feast it's the eve of" precisely
* (rather than guessing from date adjacency alone) and skip
* commemorating the Vigil redundantly alongside its own feast. */
vigilOf?: string;
/** This feast, when it keeps its own Second Vespers (see
* calendar/vespers.ts's keepsOwnSecondVespers), refuses to commemorate
* whatever wins the following day there — the feast-side mirror of a
* privileged Sunday/major-feria refusing a weak commemoration *back*
* (calendar/commemorations.ts), which vu had no equivalent of at all
* until this field. Modeled as an explicit per-feast flag rather than
* porting the reference engine's fractional-rank privilege matrix
* (crank/rank thresholds, a "Festum Domini" distinction, an outright
* hardcoded pair exception for Most Precious Blood vs. the Sacred
* Heart on the rare late-Easter date they collide) — that matches this
* repo's own "explicit per-feast fact, verified against the live
* engine one date at a time" convention (same as `vigilOf` above)
* rather than a general formula applied blanket and hoped correct.
* Absent (the default) preserves today's status quo: any sanctoral
* successor gets commemorated regardless of rank. Not yet set on any
* saint — add only after checking a specific date against the live
* reference engine, not speculatively for every `duplex-1-classis`
* feast. NOT a fit for the Precious Blood/Sacred Heart case itself
* (that exclusion is specific to one neighbor, not "never commemorate
* anyone" — see this field's own doc comment above) — see TODO.md. */
forbidsSuccessorCommemoration?: true;
} }
const sanctoralCalendar = sanctoralCalendarData as { days: Record<string, string[]> }; const sanctoralCalendar = sanctoralCalendarData as { days: Record<string, string[]> };
@@ -52,6 +141,7 @@ for (const mod of Object.values(saintModules)) {
export interface SanctoralCandidate { export interface SanctoralCandidate {
id: string; id: string;
name: string; name: string;
nameLa?: string;
rank: FeastClass; rank: FeastClass;
} }
@@ -71,6 +161,6 @@ export function getSanctoralCandidatesFor(isoDate: string): SanctoralCandidate[]
if (!saint) { if (!saint) {
throw new Error(`sanctoral-calendar.yml references unknown saint id '${id}'`); 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 };
}); });
} }
+264 -66
View File
@@ -1,19 +1,25 @@
import type { Commemoration, DayWinner, LiturgicalDay, SanctoralIdentity, TemporalCategory } from './types'; import type { Commemoration, DayWinner, FeastClass, LiturgicalDay, SanctoralIdentity, TemporalCategory } from './types';
import { weekdayOf } from './weekday'; import { weekdayOf } from './weekday';
import { resolveSeason, resolveTemporalCategory, sundayOnOrBefore } from './temporal'; import { resolveSeason, resolveTemporalCategory, easterOffsetOf } from './temporal';
import { resolveTemporalId } from './temporal-id'; import { resolveTemporalId } from './temporal-id';
import { getSanctoralCandidatesFor } from './feasts'; import { getSanctoralCandidatesFor } from './feasts';
import { decideOccurrence, isAtLeast, type OccurrenceResult } from './commemorations'; import { decideOccurrence, compareFeastClass, isAtLeast, type OccurrenceResult } from './commemorations';
import { resolveCollision } from './collision'; import { resolveCollision } from './collision';
import { addDays } from './date-math'; import { addDays, toIsoDate } from './date-math';
import { easterSunday } from './easter';
import { activeOctavesFor, strictestThreshold } from './octaves'; import { activeOctavesFor, strictestThreshold } from './octaves';
import { applyMovableFeasts } from './movable-feasts';
import { applyEmberDay } from './ember-days';
/** /**
* A day's occurrence considered on its own — no awareness of what an * 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 * adjacent day might be trying to transfer in. `resolveDay` calls this
* the date itself *and* for yesterday/tomorrow (to check for an inbound * for the date itself and for yesterday/tomorrow (to check for an
* transfer) without ever recursing into their own adjacent-day checks — * inbound transfer) without ever recursing into their own adjacent-day
* that's what keeps this from being mutually recursive. * 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 } { function resolveNativeOccurrence(isoDate: string): { temporalCategory: TemporalCategory; result: OccurrenceResult } {
const weekday = weekdayOf(isoDate); const weekday = weekdayOf(isoDate);
@@ -34,7 +40,12 @@ function resolveNativeOccurrence(isoDate: string): { temporalCategory: TemporalC
const collision = resolveCollision(candidate, topCandidate); const collision = resolveCollision(candidate, topCandidate);
clashCommemorations.push(...collision.commemorations); clashCommemorations.push(...collision.commemorations);
if (collision.winner.kind === 'sanctoral') { 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,
};
} }
} }
@@ -48,17 +59,34 @@ function applyIncomingTransfer(
temporalCategory: TemporalCategory, temporalCategory: TemporalCategory,
winner: DayWinner, winner: DayWinner,
commemorations: Commemoration[], commemorations: Commemoration[],
fromDate: string,
): DayWinner { ): DayWinner {
if (temporalCategory === 'privileged-feria-major') { if (temporalCategory === 'privileged-feria-major') {
// Deferred: a transfer landing on one of these (the concrete case is // Never a landing spot for any rank, and never even a bare
// Holy Week, right after Palm Sunday) needs its own Easter-date-keyed // commemoration (commemorations.ts's own case already guarantees
// lookup table, the same way the reference engine handles it — not // this — a `winner.kind === 'sanctoral'` day never actually happens
// modeled yet. The transfer is simply not delivered here rather than // in this category, so `decideOccurrence` alone would already
// guessed at. The lesser privileged-feria tier doesn't need this — // produce this same no-op; kept explicit as a documented short-
// ordinary collision/transfer logic is fine there, same as any // circuit). A candidate impeded here — Holy Week, the Easter Octave,
// ordinary-sunday landing. // 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; 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') { if (winner.kind === 'temporal') {
// Found via real data: a transferred-in Simplex saint was blindly // Found via real data: a transferred-in Simplex saint was blindly
// winning outright even when it landed on a privileged Sunday (Trinity // winning outright even when it landed on a privileged Sunday (Trinity
@@ -66,15 +94,84 @@ function applyIncomingTransfer(
// the *same* precedence rules a native occurrence would have used, // the *same* precedence rules a native occurrence would have used,
// not just take over because nothing else was assigned here. // not just take over because nothing else was assigned here.
const decided = decideOccurrence(temporalCategory, winner.id, candidate); const decided = decideOccurrence(temporalCategory, winner.id, candidate);
commemorations.push(...decided.commemorations); commemorations.push(...tagCommemorations(decided.commemorations));
// If the candidate fails here too (decided.transfer set), that's a // If the candidate fails here too (decided.transfer set), it's simply
// transfer chain — not modeled, same "not delivered rather than // not delivered — every category reaching this branch only ever
// guessed at" stance as the privileged-feria-major case above. // needs a single hop (see `applyEasterOctaveBacklog` for the one
return decided.winner; // multi-day case, handled separately, not through here).
return tagIfLanded(decided.winner);
} }
const collision = resolveCollision(candidate, winner); const collision = resolveCollision(candidate, winner);
commemorations.push(...collision.commemorations); commemorations.push(...tagCommemorations(collision.commemorations));
return collision.winner; 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 };
} }
/** /**
@@ -91,51 +188,104 @@ export function resolveDay(isoDate: string): LiturgicalDay {
let winner = result.winner; let winner = result.winner;
const commemorations = [...result.commemorations]; const commemorations = [...result.commemorations];
const yesterday = resolveNativeOccurrence(addDays(isoDate, -1)); const yesterdayIso = addDays(isoDate, -1);
if (yesterday.result.transfer?.direction === 'forward') { const yesterday = resolveNativeOccurrence(yesterdayIso);
winner = applyIncomingTransfer(yesterday.result.transfer.candidate, temporalCategory, winner, commemorations); // 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') { 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 = applyOctaves(isoDate, winner, commemorations);
winner = applyEmberDay(isoDate, winner, commemorations);
winner = applyMarianSaturday(isoDate, weekday, temporalCategory, winner, commemorations); winner = applyMarianSaturday(isoDate, weekday, temporalCategory, winner, commemorations);
winner = applyChristTheKing(isoDate, winner, commemorations);
winner = applyChristmasOctaveSunday(isoDate, weekday, winner, commemorations); 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;
/** The Sunday on or before Oct 31 — always lands in October since Oct 31 return { date: isoDate, weekday, season, temporalCategory, winner, commemorations, transferredAway };
* is at most 6 days after the month's last Sunday. */
function lastSundayOfOctober(year: number): string {
return sundayOnOrBefore(`${year}-10-31`);
} }
/** /**
* Per direct instruction: the last Sunday of October is always Christ the * Real rubric, confirmed 2026-08: when the 6th Sunday after Epiphany is
* King, full stop — unlike every other layer in this file, nothing here * bumped by Septuagesima — i.e. only in years where the 5th Sunday after
* loses gracefully or gets a rank check; whatever was winning (a plain * Epiphany was the last one to actually occur — it's commemorated on the
* numbered Sunday after Pentecost in every ordinary year, but modeled * Saturday immediately before Septuagesima, on top of whatever else that
* generally in case a high-ranked sanctoral candidate is ever assigned to * Saturday already resolved to. Distinct from (and doesn't imply) the
* that date too) is demoted straight to a commemoration. Confirmed live * later "resumed post-Epiphany Sunday" transfer handled in
* (see data/calendar/temporal-feasts/christ-the-king.yml): Duplex I. * temporal-id.ts's post-Pentecost overflow branch — that one applies to
* classis, well above anything that could contest it under this * *every* skipped post-Epiphany Sunday, not just VI; this Saturday
* project's own calendar. * commemoration is VI's alone. Purely additive: no `Commemoration` variant
* needed beyond the existing `{ kind: 'temporal' }` one, since
* hours/resolve-common.ts's getDayCollects already resolves any temporal
* commemoration's own `${id}-collect` for free.
*/ */
function applyChristTheKing(isoDate: string, winner: DayWinner, commemorations: Commemoration[]): DayWinner { function applyEpiphany6Commemoration(isoDate: string, commemorations: Commemoration[]): void {
const year = Number(isoDate.slice(0, 4)); const year = Number(isoDate.slice(0, 4));
if (isoDate !== lastSundayOfOctober(year)) { const septuagesimaStart = addDays(toIsoDate(easterSunday(year)), -63);
return winner; const saturdayBeforeSeptuagesima = addDays(septuagesimaStart, -1);
if (isoDate !== saturdayBeforeSeptuagesima) {
return;
} }
if (winner.kind === 'temporal') { const lastEpiphanySunday = addDays(septuagesimaStart, -7);
commemorations.push({ kind: 'temporal', id: winner.id }); if (resolveTemporalId(lastEpiphanySunday) !== 'post-epiphany-5') {
} else { return;
commemorations.push({ kind: 'sanctoral', id: winner.id, name: winner.name, rank: winner.rank }); }
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' });
} }
return { kind: 'temporal', id: 'christ-the-king' };
} }
/** /**
@@ -167,6 +317,23 @@ function applyChristmasOctaveSunday(
const day = Number(dayStr); const day = Number(dayStr);
if (day >= 26 && day <= 29 && weekday === 'sunday') { if (day >= 26 && day <= 29 && weekday === 'sunday') {
// The displaced saint isn't commemorated in place (per this
// function's own doc comment, "he reappears in full on Dec 30
// instead") — strip the sanctoral commemoration `decideOccurrence`
// already pushed for him before this override ran, the same
// candidate the Dec 30 branch below will look up again to revive.
// Only became reachable once `commemorations.ts`'s `ordinary-sunday`
// case started commemorating Semiduplex/Simplex in place instead of
// transferring them (see its own doc comment) — before that fix, a
// `transfer` signal carried the candidate forward instead of a
// commemoration, so there was nothing here to strip.
const displaced = getSanctoralCandidatesFor(isoDate)[0];
if (displaced) {
const idx = commemorations.findIndex((c) => c.kind === 'sanctoral' && c.id === displaced.id);
if (idx !== -1) {
commemorations.splice(idx, 1);
}
}
return { kind: 'temporal', id: 'christmas-octave-sunday' }; return { kind: 'temporal', id: 'christmas-octave-sunday' };
} }
@@ -190,7 +357,7 @@ function applyChristmasOctaveSunday(
if (idx !== -1) { if (idx !== -1) {
commemorations.splice(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 };
} }
} }
@@ -243,7 +410,7 @@ function applyMarianSaturday(
if (winner.rank !== 'simplex') { if (winner.rank !== 'simplex') {
return winner; 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; return MARIAN_SATURDAY;
} }
@@ -251,11 +418,36 @@ function applyMarianSaturday(
* Layered on top of everything above, not part of it: an octave doesn't * Layered on top of everything above, not part of it: an octave doesn't
* change how a single day's own precedence contest is decided, it just * change how a single day's own precedence contest is decided, it just
* (a) adds a commemoration for every octave still active on this date, and * (a) adds a commemoration for every octave still active on this date, and
* (b) occasionally overrides the winner when the occurring saint is too * (b) occasionally overrides the winner when the occurring saint doesn't
* minor to clear the strictest active octave's threshold, in which case * outrank the strictest active octave's threshold, in which case the day
* the day reverts to its own temporal identity and the saint is * reverts to its own temporal identity and the saint is commemorated
* commemorated instead — same "demoted, not dropped" shape as every other * instead — same "demoted, not dropped" shape as every other
* commemoration rule in this file. * commemoration rule in this file.
*
* A *tie against an octave's own elevated closing day* goes to the
* octave, not the occurring saint — live-verified counterexample: St.
* Hyacinth (plain Duplex, Aug 17) against St. Lawrence's own octave
* closing day that same date (also Duplex, via `closingDayRank`'s
* default) — the reference engine's own alternate block for that date is
* titled "Commemoratio S. Hyacinthi Confessoris", i.e. Hyacinth is the
* one merely commemorated there, Lawrence's own elevated closing day
* keeps the office. Matches this file's own `collision.ts` precedent for
* the analogous sanctoral-vs-sanctoral tie ("Ties favor `native` — the
* incoming feast is the guest here"): the closing day is the
* already-running incumbent's own elevated day, an occurring saint is
* the guest, and a guest needs to actually outrank it to displace it,
* not just match it.
*
* A tie against an *ordinary* (non-closing) octave day's threshold still
* favors the occurring saint, unchanged from the original behavior —
* confirmed by two already-verified, live-sourced counterexamples this
* file's own tests carry: St. Thomas of Canterbury (plain Semiduplex,
* Dec 29) wins outright against the Christmas Octave's own ordinary
* `wins: semiduplex` default that day (not its closing day, Jan 1), and
* St. Nicholas of Tolentino (plain Semiduplex, Sep 10) likewise against
* the Nativity of the BVM's octave (day 3 of 8, not closing). Only a
* closing day's own elevated rank carries the "already the incumbent"
* weight that breaks a tie in the octave's favor.
*/ */
function applyOctaves(isoDate: string, winner: DayWinner, commemorations: Commemoration[]): DayWinner { function applyOctaves(isoDate: string, winner: DayWinner, commemorations: Commemoration[]): DayWinner {
const octaves = activeOctavesFor(isoDate); const octaves = activeOctavesFor(isoDate);
@@ -264,11 +456,16 @@ function applyOctaves(isoDate: string, winner: DayWinner, commemorations: Commem
} }
let resolvedWinner = winner; let resolvedWinner = winner;
if (winner.kind === 'sanctoral' && !isAtLeast(winner.rank, strictestThreshold(octaves))) { if (winner.kind === 'sanctoral') {
const isOneOfTheseOctaves = octaves.some((o) => o.id === winner.id); const threshold = strictestThreshold(octaves);
if (!isOneOfTheseOctaves) { const cmp = compareFeastClass(winner.rank, threshold);
commemorations.push({ kind: 'sanctoral', id: winner.id, name: winner.name, rank: winner.rank }); const tiedAgainstClosingDay = cmp === 0 && octaves.some((o) => o.isClosingDay && compareFeastClass(o.wins, threshold) === 0);
resolvedWinner = { kind: 'temporal', id: resolveTemporalId(isoDate) }; 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, nameLa: winner.nameLa, rank: winner.rank });
resolvedWinner = { kind: 'temporal', id: resolveTemporalId(isoDate) };
}
} }
} }
@@ -279,7 +476,7 @@ function applyOctaves(isoDate: string, winner: DayWinner, commemorations: Commem
// again alongside itself would be redundant. // again alongside itself would be redundant.
const isOwnStartDay = octave.dayNumber === 1 && resolvedWinner.kind === 'temporal'; const isOwnStartDay = octave.dayNumber === 1 && resolvedWinner.kind === 'temporal';
if (!isSelf && !isOwnStartDay) { 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 });
} }
} }
@@ -296,10 +493,11 @@ export function isSundayOrFeast(day: LiturgicalDay): boolean {
return day.weekday === 'sunday' || day.winner.kind === 'sanctoral'; return day.weekday === 'sunday' || day.winner.kind === 'sanctoral';
} }
export { compareFeastClass, isAtLeast, decideOccurrence } from './commemorations'; export { compareFeastClass, isAtLeast, decideOccurrence, minimumOutrightWinningRank } from './commemorations';
export { resolveCollision } from './collision'; export { resolveCollision } from './collision';
export { resolveTemporalId } from './temporal-id'; export { resolveTemporalId } from './temporal-id';
export { activeOctavesFor, strictestThreshold, resolveActiveOctave } from './octaves'; export { monthWeekId } from './month-week-id';
export { activeOctavesFor, strictestThreshold, resolveActiveOctave, octaveGoverningPrivilegedDay } from './octaves';
export type { ActiveOctave } from './octaves'; export type { ActiveOctave } from './octaves';
export type { export type {
LiturgicalDay, LiturgicalDay,
+74
View File
@@ -0,0 +1,74 @@
// The reference engine's own "calendar-month/week" Matins-reading identity
// (e.g. "081" = 1st Sunday of August) — genuinely anchored to the civil
// calendar, NOT Easter-offset like every other id in temporal-id.ts. Real
// Nocturn 2 patristic content for the later post-Pentecost Sundays lives
// in the reference engine's Tempora/<MMN>-0.txt files (August-November),
// keyed this way, not by post-pentecost-NN — which month/week id governs a
// given date shifts every year with Easter's date, so there is no fixed
// post-pentecost-NN <-> month-week correspondence to hard-code. See
// hours/matins.ts's buildReadingPool, which pools this id's own authored
// content (data/propers/nocturn-readings/month-week-<id>.yml) alongside
// the ordinary temporalId-keyed pool, same dual-key precedent as
// propers/bible-plan.ts's Dec25-Jan13 calendar-date stretch.
//
// Ported from DivinumOfficium's own Date.pm::monthday() sub, Tridentine/
// pre-1955 rubrics (this app's baseline throughout — see
// calendar/temporal.ts's resolveTemporalCategory and its neighbors for the
// same choice made elsewhere). Concretely, "pre-1955" here means
// Date.pm's own $modernstyle flag is false: no 1960-rubric October
// week-III vanishing (that adjustment is entirely modernstyle-gated, so
// simply never applies here), and November's backward-from-Advent
// recount still applies for week > 0 (that part is NOT modernstyle-gated
// in the source) but its extra "collapse week 1 into week 0" step is
// skipped (that step *is* modernstyle-only).
import { addDays, dayOfWeek, daysBetween } from './date-math';
import { adventStart, sundayOnOrBefore } from './temporal';
/** The Sunday that "counts as" the 1st Sunday of `month` for this
* reckoning: the Sunday on-or-before the 1st, unless the 1st falls
* Thu-Sat (in which case that partial first week has fewer than 4 days in
* `month`, so it's skipped in favor of the following Sunday). */
function firstSundayOfLiturgicalMonth(year: number, month: number): string {
const firstOfMonth = `${year}-${String(month).padStart(2, '0')}-01`;
const sunday = sundayOnOrBefore(firstOfMonth);
return dayOfWeek(firstOfMonth) >= 4 ? addDays(sunday, 7) : sunday;
}
/**
* `"MMN"` (month, 2 digits; week, 1 digit — e.g. `"081"`), or `undefined`
* if `isoDate` falls outside the reckoning's range (before the 1st Sunday
* of August, or on/after Advent's start). Only ever non-undefined for
* August through the eve of Advent.
*/
export function monthWeekId(isoDate: string): string | undefined {
const year = Number(isoDate.slice(0, 4));
const month = Number(isoDate.slice(5, 7));
if (month < 7) return undefined;
let litMonth = 0;
const firstSundays: Partial<Record<number, string>> = {};
for (let m = 8; m <= 12; m++) {
const firstSunday = firstSundayOfLiturgicalMonth(year, m);
firstSundays[m] = firstSunday;
if (isoDate >= firstSunday) {
litMonth = m;
} else {
break;
}
}
if (litMonth === 0) return undefined;
const advent = adventStart(year);
if (litMonth > 10 && isoDate >= advent) return undefined;
let week = Math.floor(daysBetween(firstSundays[litMonth]!, isoDate) / 7);
// November: renumbered by counting backward from Advent rather than
// forward from November's own 1st Sunday, for every week after the
// first — real rubric, not modernstyle-gated (see header comment).
if (litMonth === 11 && week > 0) {
week = 4 - Math.floor((daysBetween(isoDate, advent) - 1) / 7);
}
return `${String(litMonth).padStart(2, '0')}${week + 1}`;
}
+103
View File
@@ -0,0 +1,103 @@
// A generic resolver for feasts whose own date isn't reachable through
// calendar/temporal-id.ts's ordinary governing-Sunday lookup -- either
// because the feast itself isn't a Sunday (Immaculate Heart of Mary,
// Easter+69, always a Saturday), or because its occurrence rule (last
// Sunday of a month, unconditional) doesn't fit the plain rank-contest
// every other day goes through (Christ the King).
//
// Every entry is just a `data/calendar/temporal-feasts/<id>.yml` file with
// `rank` and `anchor` set (temporal-feasts.ts's own `movableTemporalFeasts`
// scans for these) -- adding the next one (an Ember/Rogation day, a
// Sacred-Heart-family Marian feast, a Lenten Friday devotion, St. Joseph's
// own Eastertide feast, ...) needs no new function here or in
// calendar/index.ts, just a new anchor value on that new file.
//
// Deliberately doesn't try to cover every movable-date pattern in this
// codebase: `marian-saturday` (a generic "nothing else assigned" fallback,
// not date-anchored at all) and `christmas-octave-sunday` (whichever of
// Dec 26-29 is a Sunday, a fixed-date-*range* search rather than a
// Nth-Sunday-from-an-anchor one) are structurally different rules and stay
// as their own small functions in calendar/index.ts.
import type { Commemoration, DayWinner } from './types';
import type { MovableAnchor } from './temporal-feasts';
import { compareFeastClass } from './commemorations';
import { easterSunday } from './easter';
import { addDays, toIsoDate } from './date-math';
import { adventStart, sundayOnOrAfter, sundayOnOrBefore } from './temporal';
import { movableTemporalFeasts } from './temporal-feasts';
function lastDayOfMonth(year: number, month: number): string {
const nextMonthFirst = month === 12 ? toIsoDate({ year: year + 1, month: 1, day: 1 }) : toIsoDate({ year, month: month + 1, day: 1 });
return addDays(nextMonthFirst, -1);
}
/** The Nth Sunday of `month` (`nth: -1` for "last"), or the Nth Sunday
* counting from Advent's own start (always a Sunday itself, so "1st
* Sunday of Advent" is exactly `adventStart`) -- the two calendar-
* anchored kinds share one "count Sundays from an anchor date" shape,
* just with a different anchor. `{ kind: 'easter' }` needs no such
* counting at all: Easter Sunday is already a known, fixed anchor, so any
* day pegged to it (Sunday or weekday alike) is just a direct offset. */
function dateForAnchor(year: number, anchor: MovableAnchor): string {
switch (anchor.kind) {
case 'easter':
return addDays(toIsoDate(easterSunday(year)), anchor.offset);
case 'nth-sunday-of-month': {
if (anchor.nth === -1) {
return sundayOnOrBefore(lastDayOfMonth(year, anchor.month));
}
const firstSunday = sundayOnOrAfter(toIsoDate({ year, month: anchor.month, day: 1 }));
return addDays(firstSunday, 7 * (anchor.nth - 1));
}
case 'nth-sunday-of-advent':
return addDays(adventStart(year), 7 * (anchor.nth - 1));
case 'fixed-date':
return toIsoDate({ year, month: anchor.month, day: anchor.day });
}
}
/**
* Run last in resolveDay's override chain (see calendar/index.ts) so a
* specific named movable feast always supersedes whatever generic default
* (marian-saturday, a plain Sunday-of-week id) ran before it.
*
* Two occurrence rules, per feast: the ordinary one is rank-compared,
* commemorate-the-loser (a temporal winner, with no real standing of its
* own, is always superseded; a sanctoral winner keeps the day if its own
* rank is at least as strong, commemorating the movable feast instead;
* otherwise the movable feast wins and the displaced saint is
* commemorated) -- same shape applyMarianSaturday already uses. The
* `unconditional` rule (Christ the King) skips the rank contest entirely
* and always wins, commemorating whatever it displaces regardless of
* strength.
*/
export function applyMovableFeasts(isoDate: string, winner: DayWinner, commemorations: Commemoration[]): DayWinner {
const year = Number(isoDate.slice(0, 4));
let resolvedWinner = winner;
for (const feast of movableTemporalFeasts()) {
if (isoDate !== dateForAnchor(year, feast.anchor!)) {
continue;
}
const FEAST_WINNER: DayWinner = { kind: 'temporal', id: feast.id };
if (feast.unconditional) {
if (resolvedWinner.kind === 'temporal') {
commemorations.push({ kind: 'temporal', id: resolvedWinner.id });
} else {
commemorations.push({ kind: 'sanctoral', id: resolvedWinner.id, name: resolvedWinner.name, nameLa: resolvedWinner.nameLa, rank: resolvedWinner.rank });
}
resolvedWinner = FEAST_WINNER;
continue;
}
const rank = feast.rank ?? 'simplex';
if (resolvedWinner.kind === 'temporal') {
commemorations.push({ kind: 'temporal', id: resolvedWinner.id });
resolvedWinner = FEAST_WINNER;
} 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, nameLa: resolvedWinner.nameLa, rank: resolvedWinner.rank });
resolvedWinner = FEAST_WINNER;
}
}
return resolvedWinner;
}
+70 -10
View File
@@ -8,15 +8,18 @@
// contest is decided, it just adds commemorations on top of whatever that // contest is decided, it just adds commemorations on top of whatever that
// contest already produced, and occasionally overrides the winner when a // contest already produced, and occasionally overrides the winner when a
// too-minor saint would otherwise have taken the day from it. // too-minor saint would otherwise have taken the day from it.
import type { FeastClass, OctaveConfig } from './types'; import type { FeastClass, LiturgicalDay, OctaveConfig } from './types';
import { getSanctoralCandidatesFor, getSaintRecord } from './feasts'; import { getSanctoralCandidatesFor, getSaintRecord } from './feasts';
import { getTemporalFeastRecord, temporalFeastIdsStartingOn } from './temporal-feasts'; import { getTemporalFeastRecord, temporalFeastIdsStartingOn } from './temporal-feasts';
import { addDays, daysBetween } from './date-math'; import { addDays, daysBetween } from './date-math';
import { compareFeastClass } from './commemorations'; import { compareFeastClass, minimumOutrightWinningRank } from './commemorations';
export interface ActiveOctave { export interface ActiveOctave {
id: string; id: string;
name: 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` /** This octave's own effective rank *today* — the ordinary `wins`
* threshold on every day except its own closing day, where it's * threshold on every day except its own closing day, where it's
* `closingDayRank` instead (see calendar/types.ts's OctaveConfig). * `closingDayRank` instead (see calendar/types.ts's OctaveConfig).
@@ -27,6 +30,11 @@ export interface ActiveOctave {
wins: FeastClass; wins: FeastClass;
/** 1 on the feast's own day, counting up from there. */ /** 1 on the feast's own day, counting up from there. */
dayNumber: number; dayNumber: number;
/** Whether `dayNumber` is this octave's own final ("in Octava") day —
* i.e. whether `wins` above came from `closingDayRank` rather than the
* ordinary `wins` config. Used by calendar/index.ts's applyOctaves to
* decide which side a *tied* rank favors — see its own doc comment. */
isClosingDay: boolean;
} }
const DEFAULT_DAYS = 8; const DEFAULT_DAYS = 8;
@@ -44,6 +52,7 @@ function considerCandidate(
startDate: string, startDate: string,
id: string, id: string,
name: string, name: string,
nameLa: string | undefined,
octave: OctaveConfig | undefined, octave: OctaveConfig | undefined,
): void { ): void {
if (!octave?.enabled || seen.has(id)) { if (!octave?.enabled || seen.has(id)) {
@@ -58,7 +67,7 @@ function considerCandidate(
const dayNumber = offset + 1; const dayNumber = offset + 1;
const isClosingDay = dayNumber === days; const isClosingDay = dayNumber === days;
const wins = isClosingDay ? (octave.closingDayRank ?? DEFAULT_CLOSING_DAY_RANK) : (octave.wins ?? DEFAULT_WINS); const wins = isClosingDay ? (octave.closingDayRank ?? DEFAULT_CLOSING_DAY_RANK) : (octave.wins ?? DEFAULT_WINS);
active.push({ id, name, wins, dayNumber }); active.push({ id, name, nameLa, wins, dayNumber, isClosingDay });
} }
/** Every octave (sanctoral or temporal) whose window covers `isoDate`, /** Every octave (sanctoral or temporal) whose window covers `isoDate`,
@@ -75,14 +84,14 @@ export function activeOctavesFor(isoDate: string): ActiveOctave[] {
for (const candidate of getSanctoralCandidatesFor(candidateDate)) { for (const candidate of getSanctoralCandidatesFor(candidateDate)) {
const record = getSaintRecord(candidate.id); const record = getSaintRecord(candidate.id);
if (record) { 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)) { for (const feastId of temporalFeastIdsStartingOn(candidateDate)) {
const record = getTemporalFeastRecord(feastId); const record = getTemporalFeastRecord(feastId);
if (record) { 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);
} }
} }
} }
@@ -110,11 +119,19 @@ export function strictestThreshold(octaves: ActiveOctave[]): FeastClass {
* *
* - Highest effective rank (`wins`, already elevated on either octave's * - Highest effective rank (`wins`, already elevated on either octave's
* own closing day) wins outright. * own closing day) wins outright.
* - Tied rank: the more recently *started* octave wins (smaller * - Tied rank, one of them on its own closing day: the closing day wins
* `dayNumber` today) — the reasoning given was that day 1 of a newly * — its "in Octava" day is the octave's own culminating day, so it
* started octave needs to be fully present, which is the whole point * takes precedence over an ordinary day of an equally-ranked octave
* of it starting; the older octave that's already been running is * even if that other octave started more recently (2026-08 user
* commemorated instead, same as any octave that loses this comparison. * instruction, concrete case: Aug 17, St. Lawrence's own closing day,
* vs. the Assumption's ordinary day 3, both `duplex` once Assumption's
* octave was strengthened — Lawrence's closing day wins).
* - Tied rank, neither (or both) on their own closing day: the more
* recently *started* octave wins (smaller `dayNumber` today) — the
* reasoning given was that day 1 of a newly started octave needs to be
* fully present, which is the whole point of it starting; the older
* octave that's already been running is commemorated instead, same as
* any octave that loses this comparison.
* *
* Undefined when no octave is active at all. Every other active octave * Undefined when no octave is active at all. Every other active octave
* still gets commemorated regardless of which one wins here — this * still gets commemorated regardless of which one wins here — this
@@ -126,6 +143,46 @@ export function resolveActiveOctave(isoDate: string): ActiveOctave | undefined {
return pickWinningOctave(activeOctavesFor(isoDate)); return pickWinningOctave(activeOctavesFor(isoDate));
} }
/**
* Whether an active octave's own effective rank clears the bar a real
* sanctoral candidate would need to win `day`'s temporal category outright
* — i.e. whether the octave governs the day's content/label even though
* no *saint* occurring there was strong enough to (a "foreign" octave
* superimposed on a season it doesn't belong to, e.g. the Immaculate
* Conception's own octave running Dec 8-15, squarely inside Advent).
* Live-verified against Divino Afflatu 1954: Dec 9/10/12/14 (ordinary
* octave days, Semiduplex) and Dec 15 (the octave's own elevated closing
* day, Duplex majus) all win outright over Advent's own privileged-feria-
* minor ferias, exactly the same threshold `decideOccurrence` already
* uses for a real Semiduplex+ saint there (St. Nicholas, Dec 6).
*
* Deliberately excludes `ordinary-feria` (unconditional there already —
* any active octave governs regardless of rank, handled separately by
* each caller via `resolveActiveOctave` directly) and `christmastide`
* (Christmas's own stacked octaves — Christmas, St. Stephen, St. John,
* Holy Innocents — are structurally already that season's own temporal
* content, not a foreign add-on contesting it the way the Immaculate
* Conception's octave contests Advent: live-verified, Dec 30 keeps
* "Dominica Infra Octavam Nativitatis" as the real DO title regardless of
* actual weekday or which of the four octaves is active, never an octave
* name — see calendar/day-label.ts's and hours/resolve-common.ts's own
* Dec-30 doc comments for the fuller reasoning already established
* there). */
export function octaveGoverningPrivilegedDay(day: LiturgicalDay): ActiveOctave | undefined {
if (day.temporalCategory === 'ordinary-feria' || day.season === 'christmastide') {
return undefined;
}
const minRank = minimumOutrightWinningRank(day.temporalCategory);
if (!minRank) {
return undefined;
}
const octave = resolveActiveOctave(day.date);
if (octave && compareFeastClass(octave.wins, minRank) >= 0) {
return octave;
}
return undefined;
}
/** The comparison itself, factored out from resolveActiveOctave so the /** The comparison itself, factored out from resolveActiveOctave so the
* precedence rule (rank, then recency) is directly unit-testable against * precedence rule (rank, then recency) is directly unit-testable against
* synthetic ActiveOctave data — no real equal-rank overlap exists yet in * synthetic ActiveOctave data — no real equal-rank overlap exists yet in
@@ -142,6 +199,9 @@ export function pickWinningOctave(active: ActiveOctave[]): ActiveOctave | undefi
if (rankCmp < 0) { if (rankCmp < 0) {
return best; return best;
} }
if (candidate.isClosingDay !== best.isClosingDay) {
return candidate.isClosingDay ? candidate : best;
}
return candidate.dayNumber < best.dayNumber ? candidate : best; return candidate.dayNumber < best.dayNumber ? candidate : best;
}, undefined); }, undefined);
} }
+63 -2
View File
@@ -5,13 +5,58 @@
// don't need a record at all (they're just collect-text lookups via // don't need a record at all (they're just collect-text lookups via
// propers/index.ts's getTemporalProper); this only exists for the ones // propers/index.ts's getTemporalProper); this only exists for the ones
// that do. // that do.
import type { OctaveConfig } from './types'; import type { FeastClass, OctaveConfig } from './types';
import { easterOffsetOf } from './temporal'; import { easterOffsetOf } from './temporal';
/** How to compute a movable feast's own date each year, for a feast that
* ISN'T itself a Sunday (or, for `unconditional` feasts like Christ the
* King, one whose occurrence rule is simpler to express as "always wins
* outright" than as an ordinary rank contest) -- see
* calendar/movable-feasts.ts, which is the only consumer of this.
* `resolveTemporalId`'s own Easter-offset resolution only ever finds one
* of the 52 canonical *Sunday* ids (it works by finding the governing
* Sunday on-or-before a date, then looking that Sunday's own offset up);
* none of these three anchor kinds are reachable through that lookup no
* matter what, so they need this separate mechanism instead. A feast
* that *is* a Sunday itself (Pentecost) doesn't need this at all -- it's
* already covered by the ordinary Sunday lookup, and by
* EASTER_OFFSET_STARTS below if it also has an octave to start. */
export type MovableAnchor =
| { kind: 'easter'; offset: number }
| { kind: 'nth-sunday-of-month'; month: number; nth: number } // nth: 1-4, or -1 for "last"
| { kind: 'nth-sunday-of-advent'; nth: number } // nth: 1-4 (Advent always has exactly 4 Sundays)
| { kind: 'fixed-date'; month: number; day: number }; // a plain civil-calendar date, not itself Easter- or Advent-anchored (e.g. the Circumcision, Jan 1)
export interface TemporalFeastRecord { export interface TemporalFeastRecord {
id: string; id: string;
name: 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; octave?: OctaveConfig;
rank?: FeastClass;
anchor?: MovableAnchor;
/** Christ the King's own rule: always wins the day outright, no rank
* contest at all (see calendar/movable-feasts.ts's applyMovableFeasts) --
* every other movable feast instead loses to an equal-or-higher-ranked
* sanctoral winner and is commemorated alongside it instead. */
unconditional?: boolean;
/** The temporal-feast sibling of calendar/feasts.ts's SaintRecord field
* of the same name -- see that field's own doc comment for the full
* rationale (a feast-side mirror of the day-side privilege
* calendar/commemorations.ts already models, deliberately a flag
* rather than a ported rank-threshold formula). Set on sacred-heart.yml
* specifically -- live source (horascommon.pl, github #4586): on the
* rare late-Easter date the movable Feast of the Sacred Heart falls the
* day immediately before the fixed Most Precious Blood (July 1), Sacred
* Heart's own kept Second Vespers explicitly does *not* commemorate
* Precious Blood the next evening ("Vespera de præcedenti; nihil de
* sequenti") -- the reverse direction (Precious Blood's own Vespers
* commemorating a Sacred Heart that falls *after* it, the other rare
* late-Easter arrangement) has no such carve-out in the reference
* engine, so this flag is intentionally one-directional, not mirrored
* onto most-precious-blood.yml. */
forbidsSuccessorCommemoration?: true;
} }
const temporalFeastModules = import.meta.glob<{ default: TemporalFeastRecord }>( const temporalFeastModules = import.meta.glob<{ default: TemporalFeastRecord }>(
@@ -28,13 +73,29 @@ export function getTemporalFeastRecord(id: string): TemporalFeastRecord | undefi
return temporalFeastsById.get(id); return temporalFeastsById.get(id);
} }
/** Every temporal feast record carrying its own `anchor` -- the table
* calendar/movable-feasts.ts's applyMovableFeasts scans. Adding a new
* movable feast (an Ember/Rogation day, a Sacred-Heart-family Marian
* feast, a Lenten Friday devotion, ...) is just a new
* `data/calendar/temporal-feasts/<id>.yml` file with `rank` and `anchor`
* set -- no new code. */
export function movableTemporalFeasts(): TemporalFeastRecord[] {
return [...temporalFeastsById.values()].filter((f) => f.anchor !== undefined);
}
/** Fixed-calendar-date starts (MM-DD -> temporal feast id). Only Christmas /** Fixed-calendar-date starts (MM-DD -> temporal feast id). Only Christmas
* so far; Epiphany/Candlemas would join here if they ever needed an * so far; Epiphany/Candlemas would join here if they ever needed an
* octave modeled too. */ * octave modeled too. */
const FIXED_DATE_STARTS: [string, string][] = [['12-25', 'christmas-day']]; const FIXED_DATE_STARTS: [string, string][] = [['12-25', 'christmas-day']];
/** Easter-offset starts (offset -> temporal feast id). */ /** Easter-offset starts (offset -> temporal feast id). */
const EASTER_OFFSET_STARTS: [number, string][] = [[49, 'pentecost-sunday']]; const EASTER_OFFSET_STARTS: [number, string][] = [
[17, 'patronage-of-st-joseph'],
[39, 'ascension'],
[49, 'pentecost-sunday'],
[60, 'corpus-christi'],
[68, 'sacred-heart'],
];
/** Which temporal feast(s), if any, have their own (day-1) octave start on this date. */ /** Which temporal feast(s), if any, have their own (day-1) octave start on this date. */
export function temporalFeastIdsStartingOn(isoDate: string): string[] { export function temporalFeastIdsStartingOn(isoDate: string): string[] {
+56 -12
View File
@@ -11,16 +11,23 @@
// header and propers/index.ts's getTemporalProper for the fuller version // header and propers/index.ts's getTemporalProper for the fuller version
// of this reasoning. // of this reasoning.
// //
// Two known, deliberately unfixed gaps: the ferias between Christmas Day // One known, deliberately unfixed gap: the ferias between Christmas Day
// and the Sunday within its octave, and between Epiphany and its own // and the Sunday within its octave, and between Epiphany and its own
// first Sunday, fall back to that season's first named Sunday a few days // first Sunday, fall back to that season's first named Sunday a few days
// early — Christmas Day's and Epiphany's own collects aren't authored as // early — Christmas Day's and Epiphany's own collects aren't authored as
// separate temporal-propers entries (only Sunday collects were pulled), so // separate temporal-propers entries (only Sunday collects were pulled), so
// this is a deliberate approximation, not an oversight. Likewise, a real // this is a deliberate approximation, not an oversight.
// overflow year (early Easter, more than 24 weeks between Trinity and //
// Advent) would traditionally reuse the unused post-Epiphany Sundays' // Overflow years (Easter early enough that fewer than 6 Sundays after
// collects for the excess weeks — not modeled; this just clamps at // Epiphany occur before Septuagesima cuts in) are handled below: the
// post-pentecost-24. // skipped post-Epiphany Sundays "resume" after Sunday XXIII after
// Pentecost, in their own numeric order, right before the fixed final
// Sunday of the year (always Sunday XXIV's own formulary, regardless of
// how many Sundays actually elapsed since Pentecost). Ported directly
// from the reference engine's own `getweek()`
// (DivinumOfficium/Date.pm) — same rubric, same arithmetic (weeks-until-
// Advent, not a separately-tracked count of skipped Epiphany Sundays);
// confirmed against that source rather than re-derived by hand.
import { resolveSeason, sundayOnOrBefore, firstSundayStrictlyAfter, adventStart, easterOffsetOf } from './temporal'; import { resolveSeason, sundayOnOrBefore, firstSundayStrictlyAfter, adventStart, easterOffsetOf } from './temporal';
import { daysBetween } from './date-math'; import { daysBetween } from './date-math';
@@ -43,17 +50,52 @@ const EASTER_OFFSET_IDS: [number, string][] = [
[42, 'sunday-after-ascension'], [42, 'sunday-after-ascension'],
[49, 'pentecost-sunday'], [49, 'pentecost-sunday'],
]; ];
for (let n = 1; n <= 24; n++) { // Sundays I-XXII after Pentecost only — XXIII and XXIV are handled by
// resolvePostPentecost23Plus below, since (unlike I-XXII) which id a given
// week gets can depend on distance-to-Advent, not just its own offset.
for (let n = 1; n <= 22; n++) {
EASTER_OFFSET_IDS.push([56 + 7 * (n - 1), `post-pentecost-${String(n).padStart(2, '0')}`]); EASTER_OFFSET_IDS.push([56 + 7 * (n - 1), `post-pentecost-${String(n).padStart(2, '0')}`]);
} }
const EASTER_OFFSET_ID_MAP = new Map(EASTER_OFFSET_IDS); const EASTER_OFFSET_ID_MAP = new Map(EASTER_OFFSET_IDS);
const MAX_EASTER_OFFSET = EASTER_OFFSET_IDS[EASTER_OFFSET_IDS.length - 1]![0];
const MAX_EASTER_OFFSET_ID = EASTER_OFFSET_IDS[EASTER_OFFSET_IDS.length - 1]![1]; /**
* A governing Sunday at or beyond Pentecost week XXIII. `pentecostWeek` is
* the reference engine's own `$n` (1 = Trinity Sunday, verified against
* DivinumOfficium/Date.pm and its Tempora/PentNN file naming).
*/
function resolvePostPentecost23Plus(governingSunday: string, year: number, pentecostWeek: number): string {
const wdist = Math.floor((daysBetween(governingSunday, adventStart(year)) + 6) / 7);
if (wdist < 2) {
return 'post-pentecost-24';
}
if (pentecostWeek === 23) {
return 'post-pentecost-23';
}
return `post-epiphany-${8 - wdist}`;
}
export function resolveTemporalId(isoDate: string): string { export function resolveTemporalId(isoDate: string): string {
const season = resolveSeason(isoDate); const season = resolveSeason(isoDate);
const year = Number(isoDate.slice(0, 4)); 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') { if (season === 'advent') {
const start = adventStart(year); const start = adventStart(year);
const n = Math.round(daysBetween(start, sundayOnOrBefore(isoDate)) / 7) + 1; const n = Math.round(daysBetween(start, sundayOnOrBefore(isoDate)) / 7) + 1;
@@ -78,9 +120,11 @@ export function resolveTemporalId(isoDate: string): string {
// regardless of which `season` bucket the feria itself falls in (the // regardless of which `season` bucket the feria itself falls in (the
// ferias right after Ash Wednesday genuinely reuse Quinquagesima's // ferias right after Ash Wednesday genuinely reuse Quinquagesima's
// collect, crossing what resolveSeason calls two different seasons). // collect, crossing what resolveSeason calls two different seasons).
const offset = easterOffsetOf(sundayOnOrBefore(isoDate)); const governingSunday = sundayOnOrBefore(isoDate);
if (offset > MAX_EASTER_OFFSET) { const offset = easterOffsetOf(governingSunday);
return MAX_EASTER_OFFSET_ID; const pentecostWeek = Math.round((offset - 49) / 7);
if (pentecostWeek >= 23) {
return resolvePostPentecost23Plus(governingSunday, year, pentecostWeek);
} }
return EASTER_OFFSET_ID_MAP.get(offset) ?? 'septuagesima'; return EASTER_OFFSET_ID_MAP.get(offset) ?? 'septuagesima';
} }
+100 -5
View File
@@ -42,12 +42,21 @@ function fixedDateFor(id: string): { month: number; day: number } {
const CHRISTMAS = fixedDateFor('christmas-day'); const CHRISTMAS = fixedDateFor('christmas-day');
const EPIPHANY = fixedDateFor('epiphany'); const EPIPHANY = fixedDateFor('epiphany');
/** The Sunday nearest `isoDate` — ties (exactly 3-4 days either way can't
* happen; a week has one middle point, Wednesday, which is always closer
* to one side or the other) resolved the usual "nearest" way: on or
* before if `isoDate` is Sun-Wed, the next one if Thu-Sat. Shared by
* `adventStart` (nearest Nov 30) and calendar/ember-days.ts's own
* September Ember anchor (nearest Sept 14). */
export function nearestSunday(isoDate: string): string {
const dow = dayOfWeek(isoDate);
const delta = dow <= 3 ? -dow : 7 - dow;
return addDays(isoDate, delta);
}
/** The Sunday nearest Nov 30 (St. Andrew's Day) — Advent's real start rule. */ /** The Sunday nearest Nov 30 (St. Andrew's Day) — Advent's real start rule. */
export function adventStart(year: number): string { export function adventStart(year: number): string {
const nov30 = `${year}-11-30`; return nearestSunday(`${year}-11-30`);
const dow = dayOfWeek(nov30);
const delta = dow <= 3 ? -dow : 7 - dow;
return addDays(nov30, delta);
} }
/** /**
@@ -68,6 +77,15 @@ export function firstSundayStrictlyAfter(isoDate: string): string {
return addDays(isoDate, dow === 0 ? 7 : 7 - dow); return addDays(isoDate, dow === 0 ? 7 : 7 - dow);
} }
/** The Sunday on or after `isoDate` — `isoDate` itself, if it's already a
* Sunday. Used by calendar/movable-feasts.ts's "Nth Sunday of month"
* anchor (the 1st Sunday of a month counts the month's own 1st day if
* that's a Sunday, unlike firstSundayStrictlyAfter above). */
export function sundayOnOrAfter(isoDate: string): string {
const dow = dayOfWeek(isoDate);
return dow === 0 ? isoDate : addDays(isoDate, 7 - dow);
}
function seasonFromEasterOffset(offset: number): Season { function seasonFromEasterOffset(offset: number): Season {
const dayOverride = easterOffsets.days?.[String(offset)]; const dayOverride = easterOffsets.days?.[String(offset)];
if (dayOverride) { if (dayOverride) {
@@ -126,6 +144,53 @@ export function easterOffsetOf(isoDate: string): number {
return daysBetween(toIsoDate(easterSunday(year)), isoDate); return daysBetween(toIsoDate(easterSunday(year)), isoDate);
} }
/** The Sacred Triduum: Maundy/Holy Thursday through Holy Saturday
* (inclusive). Doesn't line up with `season` (which resolves this window
* to 'passiontide', same as the two weeks before it) — see
* hours/marian-antiphon.ts's isCandlemasToHolyWednesday for the same
* direct-date-check pattern used for another window `season` can't
* represent. Gloria Patri is omitted after psalms only in this narrower
* window, not throughout Passiontide/Holy Week generally. */
export function isInTriduum(isoDate: string): boolean {
const offset = easterOffsetOf(isoDate);
return offset >= -3 && offset <= -1;
}
const SEPTEMBER_EMBER_OFFSETS = [3, 5, 6]; // Wed, Fri, Sat after the anchor Sunday
const ADVENT_EMBER_OFFSETS = [3, 5, 6]; // Wed, Fri, Sat after the 3rd Sunday of Advent
/** The Sunday nearest Sept 14 (Exaltation of the Holy Cross) — anchors
* September's Ember days, same "nearest" rule as Advent's own start.
* Live-verified (2026, 2019) against the reference engine: genuinely
* "nearest," not "3rd Sunday of the calendar month" — those two
* computations disagree in some years (e.g. 2025). */
export function septemberEmberAnchor(year: number): string {
return nearestSunday(`${year}-09-14`);
}
/** Which of September's 3 Ember-day offsets (3/5/6 = Wed/Fri/Sat)
* `isoDate` is, if any — shared by resolveTemporalCategory below (the
* precedence side: these get `privileged-feria-minor`) and
* calendar/ember-days.ts (the content-id side: each gets its own proper
* collect/readings instead of inheriting the governing Sunday's). */
export function septemberEmberDayOffset(isoDate: string): number | undefined {
const year = Number(isoDate.slice(0, 4));
const anchor = septemberEmberAnchor(year);
return SEPTEMBER_EMBER_OFFSETS.find((offset) => addDays(anchor, offset) === isoDate);
}
/** Which of Advent's 3 Ember-day offsets `isoDate` is, if any. Unlike
* September's, this doesn't need its own resolveTemporalCategory entry —
* Advent's ordinary `privileged-feria-minor` season default already
* covers it (no live evidence found that Advent Ember days need a
* stronger tier) — but it still needs its own content id, same as
* September's, hence still exported for calendar/ember-days.ts. */
export function adventEmberDayOffset(isoDate: string): number | undefined {
const year = Number(isoDate.slice(0, 4));
const thirdAdventSunday = addDays(adventStart(year), 14);
return ADVENT_EMBER_OFFSETS.find((offset) => addDays(thirdAdventSunday, offset) === isoDate);
}
/** /**
* A day's precedence category under the temporal cycle alone — see * A day's precedence category under the temporal cycle alone — see
* calendar/types.ts's TemporalCategory doc comment and * calendar/types.ts's TemporalCategory doc comment and
@@ -137,12 +202,33 @@ export function easterOffsetOf(isoDate: string): number {
* the Christmas vigil (Dec 24 can land on a Sunday) — and a Sunday's own * 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 * privileged/ordinary status should win in that case regardless, so
* Sundays are resolved straight from `bySeason` without consulting the * 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 { export function resolveTemporalCategory(isoDate: string, season: Season, weekday: Weekday): TemporalCategory {
const bySeasonEntry = temporalCategories.bySeason[season]; const bySeasonEntry = temporalCategories.bySeason[season];
const base = bySeasonEntry ? (weekday === 'sunday' ? bySeasonEntry.sunday : bySeasonEntry.feria) : 'ordinary-feria'; const base = bySeasonEntry ? (weekday === 'sunday' ? bySeasonEntry.sunday : bySeasonEntry.feria) : 'ordinary-feria';
if (weekday === 'sunday') { if (weekday === 'sunday') {
if (season === 'eastertide' && [0, 7].includes(easterOffsetOf(isoDate))) {
return 'privileged-sunday';
}
return base; return base;
} }
@@ -151,6 +237,15 @@ export function resolveTemporalCategory(isoDate: string, season: Season, weekday
return fixedOverride; return fixedOverride;
} }
// September Ember days: live-verified privileged-feria-minor (Ss.
// Cornelius & Cyprian, Semiduplex, won outright there with the feria
// demoted to a commemoration) — see septemberEmberDayOffset's own doc
// comment for why this can't be expressed as a plain Easter offset or
// fixed MM-DD the way every other override in this function is.
if (septemberEmberDayOffset(isoDate) !== undefined) {
return 'privileged-feria-minor';
}
const offset = easterOffsetOf(isoDate); const offset = easterOffsetOf(isoDate);
const singleOverride = temporalCategories.offsets?.[String(offset)]; const singleOverride = temporalCategories.offsets?.[String(offset)];
+58 -8
View File
@@ -76,12 +76,16 @@ export type FeastClass =
// 1617 engine, which the original single-tier model wrongly forbade) — // 1617 engine, which the original single-tier model wrongly forbade) —
// see calendar/commemorations.ts's rules for `privileged-feria` (behaves // see calendar/commemorations.ts's rules for `privileged-feria` (behaves
// like `ordinary-sunday`: Duplex+ wins outright) vs `privileged-feria-major` // like `ordinary-sunday`: Duplex+ wins outright) vs `privileged-feria-major`
// (behaves like `privileged-sunday`: never displaced at all, confirmed by // (behaves like `privileged-sunday`: never displaced, and — corrected
// checking St. Mark, Duplex II. classis, merely commemorated rather than // 2026-09-01 — never gets even a bare commemoration at any rank either;
// winning within the Easter Octave). See data/calendar/temporal- // an earlier claim that a duplex-majus+ candidate is "merely commemorated"
// categories.yml for exactly which days fall in which tier — some of that // there, citing St. Mark within the Easter Octave, turned out to be a
// split (e.g. whether Pentecost's own Ember days share Lent's lesser tier // misreading — direct instruction confirmed nothing is ever commemorated
// or Easter's major one) is still a reconstructed guess, not verified. // 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 // `privileged-feria-minor` is a third, weaker tier, added during the
// December sanctoral pull after finding Advent's own ordinary (non-Ember) // December sanctoral pull after finding Advent's own ordinary (non-Ember)
// ferias have real standing of their own in the live engine — a Simplex // ferias have real standing of their own in the live engine — a Simplex
@@ -103,6 +107,11 @@ export type TemporalCategory =
export interface SanctoralIdentity { export interface SanctoralIdentity {
id: string; id: string;
name: 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; rank: FeastClass;
} }
@@ -156,6 +165,12 @@ export type DayWinner =
// First Vespers is being anticipated this evening (i.e. this feast // First Vespers is being anticipated this evening (i.e. this feast
// belongs to *tomorrow*, but is winning tonight's Vespers/Compline). // belongs to *tomorrow*, but is winning tonight's Vespers/Compline).
vespersFrom?: 'firstVespersOfTomorrow'; 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); } & SanctoralIdentity);
/** /**
@@ -168,8 +183,27 @@ export type DayWinner =
*/ */
export type Commemoration = export type Commemoration =
| { kind: 'temporal'; id: string } | { kind: 'temporal'; id: string }
| ({ kind: 'sanctoral' } & SanctoralIdentity) | ({
| { kind: 'octave'; id: string; name: string }; kind: 'sanctoral';
// Set by calendar/vespers.ts's resolveEveningDay when this
// commemoration exists only because Vespers/Compline crossed a day
// boundary (First Vespers anticipation, or a kept Second Vespers
// still noting the displaced neighbor) — 'today' for the real
// Divino Afflatu "commemoratio de præcedenti" case, 'tomorrow' for
// "commemoratio de sequenti". Left unset for an ordinary same-day
// 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; nameLa?: string };
export interface LiturgicalDay { export interface LiturgicalDay {
/** ISO date, e.g. "2026-08-09" */ /** ISO date, e.g. "2026-08-09" */
@@ -183,4 +217,20 @@ export interface LiturgicalDay {
winner: DayWinner; winner: DayWinner;
/** Everything else commemorated alongside the winner — see the doc comment on Commemoration. */ /** Everything else commemorated alongside the winner — see the doc comment on Commemoration. */
commemorations: 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 };
} }
+182 -19
View File
@@ -8,11 +8,13 @@
// Same reconstruction caveat as commemorations.ts/temporal-categories.yml: // Same reconstruction caveat as commemorations.ts/temporal-categories.yml:
// best-effort, not sourced from a primary text for this era, expect // best-effort, not sourced from a primary text for this era, expect
// corrections. // corrections.
import type { LiturgicalDay } from './types'; import type { Commemoration, DayWinner, LiturgicalDay } from './types';
import { resolveDay } from './index'; import { resolveDay } from './index';
import { addDays } from './date-math'; import { addDays } from './date-math';
import { easterOffsetOf } from './temporal'; import { easterOffsetOf } from './temporal';
import { isAtLeast } from './commemorations'; import { compareFeastClass, isAtLeast } from './commemorations';
import { getSaintRecord } from './feasts';
import { getTemporalFeastRecord } from './temporal-feasts';
/** /**
* Fixed/movable "feasts of the Lord" load-bearing elsewhere in this app * Fixed/movable "feasts of the Lord" load-bearing elsewhere in this app
@@ -37,28 +39,153 @@ function hasFirstVespers(day: LiturgicalDay): boolean {
if (day.weekday === 'sunday' || isMajorFixedFeastOfTheLord(day.date)) { if (day.weekday === 'sunday' || isMajorFixedFeastOfTheLord(day.date)) {
return true; return true;
} }
return day.winner.kind === 'sanctoral' && isAtLeast(day.winner.rank, 'duplex-majus'); // Live-verified (2026-08): Ss. Cyriacus/Largus/Smaragdus (Aug 8, plain
// Semiduplex) claims First Vespers over St. Donatus's Aug 7 — "Vespera
// de sequenti" in the real Monastic 1617 engine. Originally thought
// Semiduplex was the actual cutoff, but re-verified 2026-08-25: St.
// Zephyrinus (Aug 26, plain Simplex) likewise claims First Vespers over
// St. Louis's Aug 25 (also Simplex) — "Vespera de sequenti; nihil de
// præcedenti" — so *any* real sanctoral winner, however low-ranked,
// clears this bar; there's no rank floor at all, only the separate
// keepsOwnSecondVespers question of whether the day *before* it
// outranks the claim. Every FeastClass (including `vigil`, untested
// directly but the same "lowest tier" logic) now qualifies.
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 { function keepsOwnSecondVespers(day: LiturgicalDay): boolean {
if ( return (
day.weekday === 'sunday' || day.weekday === 'sunday' ||
day.temporalCategory === 'privileged-feria-minor' || day.temporalCategory === 'privileged-feria-minor' ||
day.temporalCategory === 'privileged-feria' || day.temporalCategory === 'privileged-feria' ||
day.temporalCategory === 'privileged-feria-major' || day.temporalCategory === 'privileged-feria-major' ||
isMajorFixedFeastOfTheLord(day.date) isMajorFixedFeastOfTheLord(day.date)
) { );
return true;
}
return day.winner.kind === 'sanctoral' && isAtLeast(day.winner.rank, 'duplex-2-classis');
} }
function anticipated(tomorrow: LiturgicalDay): LiturgicalDay { /**
if (tomorrow.winner.kind === 'sanctoral') { * Today lost the evening to tomorrow's First Vespers — is today still
return { ...tomorrow, winner: { ...tomorrow.winner, vespersFrom: 'firstVespersOfTomorrow' } }; * commemorated there? Not if it's Simplex (or a bare temporal winner,
* with no standing of its own — same "no standing to defend" idea
* commemorations.ts's `ordinary-feria` case already uses during the
* day). Live-verified:
* - Aug 8 -> 9 (Ss. Cyriaci/Largi/Smaragdi, Semiduplex, before an
* ordinary Sunday): today loses the evening, today is still
* commemorated ("commemoratio de præcedenti").
* - Aug 7 -> 8 (St. Donatus, Simplex, before Ss. Cyriaci/etc.): today
* loses the evening and is *not* commemorated ("nihil de
* præcedenti") — pins down the Simplex exclusion specifically for
* this direction.
*/
function commemorationOfDisplacedPredecessor(loser: DayWinner): Commemoration | undefined {
if (loser.kind !== 'sanctoral' || loser.rank === 'simplex') {
return undefined;
} }
return tomorrow; return { kind: 'sanctoral', id: loser.id, name: loser.name, nameLa: loser.nameLa, rank: loser.rank, vespersNote: 'today' };
}
/**
* Today keeps its own Second Vespers regardless — is tomorrow still
* commemorated there? Unlike the reverse direction above, Simplex is
* NOT excluded here: live-verified Aug 24 -> 25 (St. Bartholomew,
* Duplex II. classis, keeps his own evening; St. Louis, Simplex, on
* Aug 25) still reads "commemoratio de sequenti" — the same asymmetry
* this file's earlier version only guessed at by symmetry with the
* other direction and flagged as unverified. Also verified with a
* non-Simplex loser: Jul 25 -> 26 (St. James, Duplex II. classis, kept
* second Vespers regardless; St. Anne, Duplex, loses the evening but is
* still commemorated).
*
* `winner` (today's own, the one actually keeping this evening) can
* refuse the commemoration outright via its own record's
* `forbidsSuccessorCommemoration` — the feast-side mirror of the
* day-side privilege calendar/commemorations.ts already models (see that
* field's own doc comment, on both SaintRecord and TemporalFeastRecord,
* for why this is a flag rather than a rank threshold). Checked on
* whichever kind of record `winner` actually is — set today only on
* `sacred-heart` (a `temporal` winner), the live-verified case that
* motivated this field; every sanctoral saint is still unset, a no-op
* until a specific date is verified and flagged.
*/
function commemorationOfDisplacedSuccessor(winner: DayWinner, loser: DayWinner): Commemoration | undefined {
if (loser.kind !== 'sanctoral') {
return undefined;
}
const forbids =
winner.kind === 'sanctoral' ? getSaintRecord(winner.id)?.forbidsSuccessorCommemoration : getTemporalFeastRecord(winner.id)?.forbidsSuccessorCommemoration;
if (forbids) {
return undefined;
}
return { kind: 'sanctoral', id: loser.id, name: loser.name, nameLa: loser.nameLa, rank: loser.rank, vespersNote: 'tomorrow' };
}
function tagVespersFrom(tomorrow: LiturgicalDay): LiturgicalDay {
if (tomorrow.winner.kind !== 'sanctoral') {
return tomorrow;
}
return { ...tomorrow, winner: { ...tomorrow.winner, vespersFrom: 'firstVespersOfTomorrow' } };
}
/** Today's own Vigil isn't commemorated alongside the very feast it's the
* eve of — e.g. Nov 29's Vigil of St. Andrew shouldn't also show up next
* to St. Andrew himself once this evening has already adopted his First
* Vespers, whether St. Andrew governs it outright or is merely
* commemorated there himself (2025-11-30: he's only commemorated under
* Advent I, a `privileged-sunday`, per commemorations.ts's own threshold
* — the Vigil would otherwise show up redundantly alongside him even
* though neither one is tomorrow's actual winner). Matched via each
* Vigil's own explicit `vigilOf` field (calendar/feasts.ts's
* `SaintRecord`) against tomorrow's winner *and* its commemorations, not
* date adjacency or name-guessing — several vigils' own ids don't map
* cleanly to their feast's id (`vigil-of-the-assumption` vs `assumption`,
* `vigil-of-st-james` vs `st-james-the-greater`), so this needs the
* explicit link. Scoped to this evening-anticipation merge specifically,
* not `commemorationOfDisplacedPredecessor` itself — the reverse direction
* (`keepsOwnSecondVespers`, commemorating *tomorrow's* Vigil while today
* keeps its own Vespers) can't hit this collision: a Vigil dated tomorrow
* belongs to a feast the day after tomorrow, never today. */
function isVigilOfTomorrow(today: LiturgicalDay, tomorrow: LiturgicalDay): boolean {
if (today.winner.kind !== 'sanctoral' || today.winner.rank !== 'vigil') {
return false;
}
const feastId = getSaintRecord(today.winner.id)?.vigilOf;
if (!feastId) {
return false;
}
return (
(tomorrow.winner.kind === 'sanctoral' && tomorrow.winner.id === feastId) ||
tomorrow.commemorations.some((c) => c.kind === 'sanctoral' && c.id === feastId)
);
}
/** The ordinary anticipation path: tag tomorrow's identity, then layer in
* today's commemoration if it's eligible (see
* commemorationOfDisplacedPredecessor). Not used by the
* isMajorFixedFeastOfTheLord override below — whether Christmas/
* Easter/etc. commemorate the day they're displacing is a separate,
* unverified question (these may well suppress it the way
* privileged-sunday/privileged-feria-major do during the day — see
* commemorations.ts), so it deliberately doesn't inherit this behavior. */
function anticipated(today: LiturgicalDay, tomorrow: LiturgicalDay): LiturgicalDay {
const tagged = tagVespersFrom(tomorrow);
const commemoration = isVigilOfTomorrow(today, tomorrow) ? undefined : commemorationOfDisplacedPredecessor(today.winner);
if (!commemoration) {
return tagged;
}
return { ...tagged, commemorations: [...tagged.commemorations, commemoration] };
} }
/** /**
@@ -67,7 +194,12 @@ function anticipated(tomorrow: LiturgicalDay): LiturgicalDay {
* Vespers *and* today doesn't keep its own Second Vespers regardless — in * Vespers *and* today doesn't keep its own Second Vespers regardless — in
* which case returns tomorrow's `LiturgicalDay`, with `vespersFrom: * which case returns tomorrow's `LiturgicalDay`, with `vespersFrom:
* 'firstVespersOfTomorrow'` set on its winner (if sanctoral) so callers * 'firstVespersOfTomorrow'` set on its winner (if sanctoral) so callers
* can tell the difference from an ordinary day. * can tell the difference from an ordinary day. Either way, whichever side
* doesn't govern the evening is folded into the returned day's own
* `commemorations` when it's eligible (see
* commemorationOfDisplacedPredecessor/-Successor) — except under the
* isMajorFixedFeastOfTheLord override just below, which doesn't apply
* that at all yet.
* *
* One deliberate absolute exception, caught by a failing test: a day on * One deliberate absolute exception, caught by a failing test: a day on
* `isMajorFixedFeastOfTheLord`'s list always wins tomorrow's Vespers, * `isMajorFixedFeastOfTheLord`'s list always wins tomorrow's Vespers,
@@ -77,19 +209,50 @@ function anticipated(tomorrow: LiturgicalDay): LiturgicalDay {
* would likewise displace an actual Sunday if one ever landed on Dec 24 or * 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 * an Ember Saturday. This is why it's a short, explicit list rather than a
* threshold: these particular days are understood to be *absolute*. * 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 { export function resolveEveningDay(isoDate: string): LiturgicalDay {
const today = resolveDay(isoDate); const today = resolveDay(isoDate);
const tomorrow = resolveDay(addDays(isoDate, 1)); const tomorrow = resolveDay(addDays(isoDate, 1));
if (isMajorFixedFeastOfTheLord(tomorrow.date)) { if (isMajorFixedFeastOfTheLord(tomorrow.date)) {
return anticipated(tomorrow); return tagVespersFrom(tomorrow);
} }
let todayWins: boolean;
if (keepsOwnSecondVespers(today)) { if (keepsOwnSecondVespers(today)) {
return 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 (!hasFirstVespers(tomorrow)) {
return today; 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
// direction doesn't exclude Simplex the way the reverse one does.
const commemoration = commemorationOfDisplacedSuccessor(today.winner, tomorrow.winner);
return commemoration ? { ...today, commemorations: [...today.commemorations, commemoration] } : today;
} }
return anticipated(tomorrow); return anticipated(today, tomorrow);
} }
@@ -11,6 +11,7 @@
# is correct. # is correct.
id: all-saints-of-the-benedictine-order id: all-saints-of-the-benedictine-order
name: "All Saints of the Order of St. Benedict" name: "All Saints of the Order of St. Benedict"
nameLa: "Omnes Sancti Ordinis Sancti Benedicti"
rank: duplex-2-classis rank: duplex-2-classis
common: proper-to-all-saints common: proper-to-all-saints
propers: null propers: "all-saints-of-the-benedictine-order"
+1
View File
@@ -5,6 +5,7 @@
# was sourced). # was sourced).
id: all-saints id: all-saints
name: "All Saints" name: "All Saints"
nameLa: "Omnes Sancti"
rank: duplex-1-classis rank: duplex-1-classis
common: proper-to-all-saints common: proper-to-all-saints
propers: "all-saints" propers: "all-saints"
+15
View File
@@ -0,0 +1,15 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/03-25.txt` -- "In Annuntiatione
# Beatæ Mariæ Virginis;;Duplex I classis;;6.92;;ex C11". Own proper
# [Oratio]/[Ant 1]/Nocturn 2-3 content of her own (real, unique text
# throughout), not a bare Common fallback -- same pattern as
# our-lady-of-the-snows.yml's siblings (assumption, immaculate-
# conception, nativity-bvm). One of the four "fixed BVM feast, excluded
# per the usual convention" items literally named alongside Purification
# 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. # collect and antiphon.
id: apparition-of-st-michael id: apparition-of-st-michael
name: "The Apparition of St. Michael the Archangel" name: "The Apparition of St. Michael the Archangel"
nameLa: "Apparitio Sancti Michaelis Archangeli"
rank: duplex-majus rank: duplex-majus
common: common-of-an-angel common: common-of-an-angel
propers: "apparition-of-st-michael" propers: "apparition-of-st-michael"
@@ -0,0 +1,14 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/02-11.txt` -- "In Apparitione
# Beatæ Mariæ Virginis Immaculatæ;;Duplex majus;;4;;ex C11" (Our Lady of
# Lourdes). Own proper [Oratio]/[Ant 1]/Nocturn 2 vita (the Bernadette
# apparition narrative); her Nocturn 3 is a direct `@Sancti/12-10bmv`
# cross-reference -- shares content with the 3rd day within the Octave
# of the Immaculate Conception (a different reading than
# 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"
+12
View File
@@ -6,9 +6,21 @@
# specific block) rather than plain text, so left unpulled this pass. # specific block) rather than plain text, so left unpulled this pass.
id: assumption id: assumption
name: "The Assumption of the Blessed Virgin Mary" name: "The Assumption of the Blessed Virgin Mary"
nameLa: "Assumptio Beatæ Mariæ Virginis"
rank: duplex-1-classis rank: duplex-1-classis
common: common-of-the-bvm common: common-of-the-bvm
propers: "assumption" propers: "assumption"
# Has a real octave (Aug 15-22) -- see calendar/types.ts's OctaveConfig. # Has a real octave (Aug 15-22) -- see calendar/types.ts's OctaveConfig.
# Strengthened to `wins: duplex` (2026-08 user decision), same pattern
# as pentecost-sunday.yml's own override -- a Semiduplex saint no
# longer just ties (and, by the ordinary-day tie rule, wins) an
# ordinary octave day here; it loses outright to the octave instead.
# Concrete motivation: st-john-eudes.yml's demotion to Semiduplex only
# actually cedes Aug 19 to the octave because of this. Side effect
# accepted by the user: st-damasus.yml (Dec 11, Immaculate Conception's
# octave) and st-nicholas-of-tolentino.yml (Sep 10, Nativity of the
# BVM's octave) are unrelated octaves not touched by this change, so
# they're unaffected -- this only strengthens Assumption's own octave.
octave: octave:
enabled: true enabled: true
wins: duplex
@@ -5,6 +5,7 @@
# identical text to Rome's, both intentionally duplicated rather than shared. # identical text to Rome's, both intentionally duplicated rather than shared.
id: chair-of-st-peter-at-antioch id: chair-of-st-peter-at-antioch
name: "The Chair of St. Peter at Antioch" name: "The Chair of St. Peter at Antioch"
nameLa: "Cathedra Sancti Petri Antiochiæ"
rank: duplex-majus rank: duplex-majus
common: common-of-an-apostle common: common-of-an-apostle
propers: "chair-of-st-peter-at-antioch" propers: "chair-of-st-peter-at-antioch"
@@ -8,6 +8,7 @@
# since both Chair feasts share it). # since both Chair feasts share it).
id: chair-of-st-peter-at-rome id: chair-of-st-peter-at-rome
name: "The Chair of St. Peter at Rome" name: "The Chair of St. Peter at Rome"
nameLa: "Cathedra Sancti Petri Romæ"
rank: duplex-majus rank: duplex-majus
common: common-of-an-apostle common: common-of-an-apostle
propers: "chair-of-st-peter-at-rome" propers: "chair-of-st-peter-at-rome"
@@ -5,6 +5,12 @@
# as its own record. Own proper collect and antiphon. # as its own record. Own proper collect and antiphon.
id: commemoration-of-st-paul id: commemoration-of-st-paul
name: "Commemoration of St. Paul, Apostle" name: "Commemoration of St. Paul, Apostle"
nameLa: "Commemoratio Sancti Pauli Apostoli"
rank: duplex rank: duplex
common: common-of-an-apostle common: common-of-an-apostle
propers: "commemoration-of-st-paul" propers: "commemoration-of-st-paul"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (06-30.txt) --
# [Rule]/[Rank] point to Commune/C1 instead, the same C-number family already
# live-query-verified for common-of-apostles.yml (see TODO.md's 'minor hour
# content gaps' section).
minorHoursCommon: common-of-apostles
@@ -3,6 +3,7 @@
# proper collect and antiphon. # proper collect and antiphon.
id: conversion-of-st-paul id: conversion-of-st-paul
name: "The Conversion of St. Paul, Apostle" name: "The Conversion of St. Paul, Apostle"
nameLa: "Conversio Sancti Pauli Apostoli"
rank: duplex-majus rank: duplex-majus
common: common-of-an-apostle common: common-of-an-apostle
propers: "conversion-of-st-paul" propers: "conversion-of-st-paul"
@@ -2,12 +2,15 @@
# kalendar check (2025-08-29) -- winner outright, Duplex, over the stub # kalendar check (2025-08-29) -- winner outright, Duplex, over the stub
# St. Sabina (see st-sabina.yml). About St. John the Baptist's beheading, # 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. # 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. # Own proper collect and antiphon.
id: decollation-of-st-john-baptist id: decollation-of-st-john-baptist
name: "The Beheading of St. John the Baptist" name: "The Beheading of St. John the Baptist"
nameLa: "Decollatio Sancti Joannis Baptistæ"
rank: duplex rank: duplex
common: common-of-an-apostle common: common-of-a-martyr
propers: "decollation-of-st-john-baptist" 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
# already live-query-verified for common-of-a-martyr.yml (see TODO.md's
# 'minor hour content gaps' section).
minorHoursCommon: common-of-a-martyr
@@ -0,0 +1,30 @@
# Read directly from the reference engine (`../divinum-officium-reference`,
# per memory vu-reference-engine-file-layout, not a live query): untagged/
# default [Rank] block of `web/www/horas/Latin/Sancti/11-09.txt` -- "In
# Dedicatione Archibasilicæ Ss. Salvatoris;;Duplex II. classis;;5;;ex C8".
# Previously excluded entirely under a stale "fixed feast of the Lord,
# not modeled" note in sanctoral-calendar.yml's own November comment --
# wrong, same class of error as our-lady-of-the-snows.yml: she has real,
# unique Lectio1-6(+6a) content of her own (the history of the Lateran
# basilica's dedication under Pope St. Sylvester, through its later
# restorations), and her `ex C8` Rule just means her collect/antiphon/
# Nocturn-3-Gospel-and-homily come from the existing Common of a
# Dedication (`collect-c8.yml`, `benedictus-antiphon-common-of-a-
# dedication.yml` -- already authored, reused directly here, not
# duplicated) -- the same Common already used by
# 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
collectCommon: collect-c8
# collect-c8's own text has no {N} placeholder to substitute (it's about
# the building, not a saint) -- collectName is still required by
# getDayCollect's own lookup, but its value is never actually used.
collectName:
la: []
en: []
minorHoursCommon: common-of-a-dedication
benedictusCommon: common-of-a-dedication
@@ -8,6 +8,13 @@
# file at this MM-DD slot (Sancti/11-18.txt) is empty. # file at this MM-DD slot (Sancti/11-18.txt) is empty.
id: dedication-of-the-basilicas-of-ss-peter-and-paul id: dedication-of-the-basilicas-of-ss-peter-and-paul
name: "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 rank: duplex
common: common-of-an-apostle common: common-of-an-apostle
propers: null propers: null
collectCommon: collect-c8
collectName:
la: []
en: []
minorHoursCommon: common-of-a-dedication
benedictusCommon: common-of-a-dedication
@@ -0,0 +1,21 @@
# Read directly from the reference engine (`../divinum-officium-reference`,
# per memory vu-reference-engine-file-layout, not a live query): untagged/
# default [Rank] block of `web/www/horas/Latin/Sancti/09-14.txt` -- "In
# Exaltatione Sanctæ Crucis;;Duplex majus;;4;;ex Sancti/05-03". Previously
# excluded entirely under a stale "fixed feast of the Lord, same
# convention as the Finding of the Holy Cross" note in
# sanctoral-calendar.yml's own September comment -- wrong, same class of
# error, now fixed alongside it (see finding-of-the-holy-cross.yml). Her
# own [Rule] is `ex Sancti/05-03` -- she reuses the Finding of the Holy
# Cross's content wholesale, so `propers` points at that saint's own
# collect/antiphon files directly rather than duplicating them (same
# cross-id-reuse pattern as st-michael.yml's `apparition-of-st-michael`).
# The Matins nocturn-readings mechanism has no equivalent cross-id
# lookup, so exaltation-of-the-holy-cross.yml duplicates
# 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,11 @@
# Dec 26 antiphon reused verbatim, no substitution this time. # Dec 26 antiphon reused verbatim, no substitution this time.
id: finding-of-st-stephen id: finding-of-st-stephen
name: "The Finding of St. Stephen, Protomartyr" name: "The Finding of St. Stephen, Protomartyr"
nameLa: "Inventio Sancti Stephani Protomartyris"
rank: semiduplex rank: semiduplex
common: common-of-a-martyr common: common-of-a-martyr
propers: "finding-of-st-stephen" propers: "finding-of-st-stephen"
# P/T/S/N: [Rule] points directly at st-stephen-protomartyr's own file
# (Sancti/12-26), not a Commune category -- reuses his own already-authored
# proper P/T/S/N wholesale (see TODO.md's 'minor hour content gaps' section).
minorHoursCommon: st-stephen-protomartyr
@@ -0,0 +1,19 @@
# Read directly from the reference engine (`../divinum-officium-reference`,
# per memory vu-reference-engine-file-layout, not a live query): untagged/
# default [Rank] block of `web/www/horas/Latin/Sancti/05-03.txt` --
# "Inventione Sanctæ Crucis;;Duplex II classis;;5.2;;". Previously
# excluded entirely under a stale "fixed feast of the Lord, not modeled"
# note in sanctoral-calendar.yml's own May comment -- wrong, same class
# of error as our-lady-of-the-snows.yml: her [Rule] says `proprium` -- a
# fully proper feast with real, complete unique text throughout
# (collect, antiphon, 8 Matins lessons -- the file itself has no
# Lectio9 at all, an unusual but genuine structural feature, not an
# omission on this app's part). `Sancti/09-14.txt`'s Exaltation of the
# Holy Cross reuses this same content wholesale (`ex Sancti/05-03`) --
# 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,12 @@
# Own proper collect, no unique antiphon of their own. # Own proper collect, no unique antiphon of their own.
id: forty-holy-martyrs id: forty-holy-martyrs
name: "The Forty Holy Martyrs" name: "The Forty Holy Martyrs"
nameLa: "Quadraginta Sancti Martyres"
rank: semiduplex rank: semiduplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "forty-holy-martyrs" propers: "forty-holy-martyrs"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (03-10.txt) --
# [Rule]/[Rank] point to Commune/C3 instead, the same C-number family already
# live-query-verified for common-of-several-martyrs.yml (see TODO.md's 'minor
# hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -0,0 +1,14 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/10-02.txt` -- "Duplex majus;;4;;
# vide C4" (Common of a Confessor Bishop -- structural category only;
# the feast's own content is fully proper throughout). Own real
# [Oratio] and own [Ant 1]. Own Nocturn 2-3 content (Nocturn 2: St.
# Bernard's own homily on Psalm 90 "Qui habitat"; Nocturn 3: St.
# Hilary's own homily, Gospel bare incipit). Nocturn 1 (Lectio1-3,
# 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. # this pull has found. Own proper collect and antiphon.
id: holy-innocents id: holy-innocents
name: "The Holy Innocents, Martyrs" name: "The Holy Innocents, Martyrs"
nameLa: "Sancti Innocentes, Martyres"
rank: duplex-2-classis rank: duplex-2-classis
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "holy-innocents" propers: "holy-innocents"
@@ -0,0 +1,16 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/09-12.txt` -- "S. Nominis
# Beatæ Mariæ Virginis;;Duplex majus;;4;;ex C11". Own real [Oratio]
# and own untagged [Ant 1] (shared verbatim with our-lady-of-mount-
# carmel-antiphon.yml -- a coincidental reuse of the same formulaic
# BVM-commemoration text already present in the reference source
# itself, not a copy-paste error introduced here). Own Nocturn 2-3
# content (Nocturn 3 is St. Peter Chrysologus's own homily on the
# Annunciation, not a cross-reference). Instituted 1683 by Innocent XI
# 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,10 +4,17 @@
# collect and antiphon. # collect and antiphon.
id: immaculate-conception id: immaculate-conception
name: "The Immaculate Conception of the Blessed Virgin Mary" name: "The Immaculate Conception of the Blessed Virgin Mary"
nameLa: "Immaculata Conceptio Beatæ Mariæ Virginis"
rank: duplex-1-classis rank: duplex-1-classis
common: common-of-the-bvm common: common-of-the-bvm
propers: "immaculate-conception" propers: "immaculate-conception"
minorHoursCommon: common-of-the-bvm minorHoursCommon: common-of-the-bvm
# Has a real octave (Dec 8-15) -- see calendar/types.ts's OctaveConfig. # Has a real octave (Dec 8-15) -- see calendar/types.ts's OctaveConfig.
# Strengthened to `wins: duplex` (2026-08 user decision), same pattern
# as pentecost-sunday.yml's and assumption.yml's own overrides. Side
# effect accepted by the user: St. Damasus (Dec 11, semiduplex) no
# longer ties and wins an ordinary octave day here -- he now loses
# outright to the octave instead (see st-damasus.yml).
octave: octave:
enabled: true enabled: true
wins: duplex
@@ -0,0 +1,14 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/10-11.txt` -- "Maternitatis
# Beatæ Mariæ Virginis;;Duplex 2 classis;;5.1;;ex C11". Own real
# [Oratio] and own [Ant 1]. Own Nocturn 2-3 content (Nocturn 2: two
# excerpts from Pope St. Leo the Great's own Nativity sermons; Nocturn
# 3: St. Bernard's own homily, Gospel bare incipit). Instituted 1931 by
# Pius XI for the 1500th anniversary of the Council of Ephesus (see the
# 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"
@@ -0,0 +1,15 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/07-01.txt` -- "Duplex I
# classis;;6;;" (`[Rule]` says `proprium` -- "Festum Domini"). A fully
# proper feast of the Lord with real, complete unique text throughout
# (own Oratio, own Ant 1, full 9-lesson Matins), same pattern as
# transfiguration.yml/finding-of-the-holy-cross.yml. Instituted by Pius
# IX in 1849, raised to Duplex I Classis by Pius XI for the 1933
# nineteen-century jubilee of the Redemption (see the feast's own
# 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,10 +5,18 @@
# wins outright over the stub St. Hadrian (see st-hadrian.yml). # wins outright over the stub St. Hadrian (see st-hadrian.yml).
id: nativity-bvm id: nativity-bvm
name: "The Nativity of the Blessed Virgin Mary" name: "The Nativity of the Blessed Virgin Mary"
nameLa: "Nativitas Beatæ Mariæ Virginis"
rank: duplex-2-classis rank: duplex-2-classis
common: common-of-the-bvm common: common-of-the-bvm
propers: "nativity-bvm" propers: "nativity-bvm"
minorHoursCommon: common-of-the-bvm minorHoursCommon: common-of-the-bvm
# Has a real octave (Sep 8-15) -- see calendar/types.ts's OctaveConfig. # Has a real octave (Sep 8-15) -- see calendar/types.ts's OctaveConfig.
# Strengthened to `wins: duplex` (2026-08 user decision), same pattern
# as pentecost-sunday.yml's and assumption.yml's own overrides. Side
# effect accepted by the user: St. Nicholas of Tolentino (Sep 10,
# semiduplex) no longer ties and wins an ordinary octave day here -- he
# now loses outright to the octave instead (see
# st-nicholas-of-tolentino.yml).
octave: octave:
enabled: true enabled: true
wins: duplex
@@ -3,12 +3,22 @@
# classis, the highest rank pulled in this set. About St. John the # 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 # 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 # sanctoral store like the Conversion of St. Paul/Chair-of-Peter feasts
# despite the very high rank. Own proper collect and antiphon. "common- # despite the very high rank. Own proper collect and antiphon.
# 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" -- # `common: common-of-an-apostle` is a placeholder, not a liturgical
# worth reconsidering if a more precise category becomes worth adding. # 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 id: nativity-of-st-john-the-baptist
name: "The Nativity of St. John the Baptist" name: "The Nativity of St. John the Baptist"
nameLa: "Nativitas Sancti Joannis Baptistæ"
rank: duplex-1-classis rank: duplex-1-classis
common: common-of-an-apostle common: common-of-an-apostle
propers: "nativity-of-st-john-the-baptist" propers: "nativity-of-st-john-the-baptist"
@@ -0,0 +1,19 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/11-08.txt` -- "In Octava Omnium
# Sanctorum;;Duplex majus;;4.1;;vide Sancti/11-01" (cross-references
# All Saints' own file for Rank/Rule). No [Oratio] at all in this
# file, unlike octave-of-ss-peter-and-paul.yml -- collect reuses All
# Saints' own real proper collect via the cross-id `propers` pointer
# (same reasoning as the Finding/Exaltation of the Holy Cross and
# Gabriel/Raphael Archangel pattern). But this day does have genuine
# own Nocturn 2-3 content (St. Cyprian's "De mortalitate", St.
# Augustine's homily on the Beatitudes) -- see
# nocturn-readings/octave-of-all-saints.yml, since the Matins
# nocturn-readings mechanism has no cross-id lookup the way
# 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"
@@ -0,0 +1,24 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/07-06.txt` -- "In Octava Ss.
# Apostolorum Petri et Pauli;;Duplex majus;;4.1;;vide C1" (Common of
# Apostles). The Octave Day itself of Ss. Peter and Paul (June 29 + 7),
# with its own real proper [Oratio] and own Nocturn 2-3 content
# (Chrysostom's sermon on both Apostles; Jerome's homily on Christ
# walking on the water). Its [Ant 1]/[Ant 2]/[Ant 3] are all a
# cross-reference (`@Sancti/07-03oct`) to the file for the Octave's
# 5th day, which has its own real proper antiphon text -- authored
# here directly since it is the actual text this day uses, not a
# private duplicate (the Matins nocturn-readings mechanism has no
# cross-id lookup the way collect/antiphon do via `propers`, so this
# mirrors the same reasoning as exaltation-of-the-holy-cross.yml's
# collect/antiphon pointer, just inlined since there's no saint id to
# point at here). Rest of the Octave's own continuation days (July 4,
# 7-8 minus named saints) aren't modeled -- pure octave-continuation
# content remains a separate, not-yet-built mechanism (see
# 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"
@@ -0,0 +1,16 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/07-16.txt` -- "In
# Commemoratione Beatæ Mariæ Virgine de Monte Carmelo;;Duplex majus;;
# 4;;ex C11". Own real [Oratio] and own untagged [Ant 1] ("Sancta
# María, succúrre míseris..." -- a separate `(rubrica cisterciensis)`
# variant cross-references `@CommuneCist/C11` instead, ignored per the
# untagged-block convention). Own Nocturn 2 vita content. No Lectio7-9
# in her own file (only a `Responsory7` cross-ref) -- defers to the
# Common of the BVM's own Nocturn 3, not transcribed here, same pattern
# 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"
@@ -0,0 +1,13 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/09-24.txt` -- "Beatæ Mariæ
# Virginis de Mercede;;Duplex majus;;4;;ex C11". Own real [Oratio].
# No [Ant 1] of her own in this file -- benedictusCommon falls back to
# the plain Common of the BVM. No Lectio7-9 -- defers to the Common's
# 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"
benedictusCommon: common-of-the-bvm
@@ -0,0 +1,32 @@
# Read directly from the reference engine (`../divinum-officium-reference`,
# per memory vu-reference-engine-file-layout, not a live query): untagged/
# default [Rank] block of `web/www/horas/Latin/Sancti/08-05.txt` --
# "Sanctæ Mariæ Virginis ad Nives;;Duplex majus;;4;;ex C11". Previously
# excluded from this app's calendar entirely under a stale "fixed BVM
# feasts aren't modeled" note in sanctoral-calendar.yml's own August
# comment -- wrong: this app already models three other fixed BVM feasts
# the same way (assumption, immaculate-conception, nativity-bvm), and
# this one has real, complete content too (Matins readings, sourced
# directly; collect/antiphon via the Common-of-the-BVM fallback her own
# `[Rule]` already points to, `ex C11` -- see collect-c11.yml and
# benedictus-antiphon-common-of-the-bvm.yml). No `propers` of her own:
# unlike the other three BVM feasts (each of which has real unique
# collect/antiphon text), her file has none, so `collectCommon`/
# `benedictusCommon` is the correct mechanism here, same as any other
# `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
collectCommon: collect-c11
# collect-c11's own text has no {N} placeholder to substitute (it
# addresses "fámulis tuis" generically, not the saint by name) --
# collectName is still required by getDayCollect's own lookup, but its
# value is never actually used for this particular Common.
collectName:
la: []
en: []
minorHoursCommon: common-of-the-bvm
benedictusCommon: common-of-the-bvm
@@ -0,0 +1,25 @@
# Read directly from the reference engine (`../divinum-officium-reference`,
# per memory vu-reference-engine-file-layout, not a live query): untagged/
# default [Rank] block of `web/www/horas/Latin/Sancti/11-21.txt` -- "In
# Præsentatione Beatæ Mariæ Virginis;;Duplex majus;;4;;ex C11". Previously
# excluded entirely under a stale "fixed BVM feast, excluded per the
# usual convention" note in sanctoral-calendar.yml's own November comment
# -- wrong, same class of error as our-lady-of-the-snows.yml. Unlike her,
# this feast DOES have a real, unique [Oratio] of its own (see
# presentation-of-the-bvm-collect.yml) and Nocturn 2 vita content
# (Lectio4-6, St. John Damascene + St. Ambrose on the Blessed Virgin).
# Her [Ant 1] is a substituted Common text (`@Commune/C10:Ant 2_`, with a
# truncate-and-add-alleluia edit specific to this feast -- see
# presentation-of-the-bvm-antiphon.yml), effectively proper by the same
# "own [Name]/edit makes shared-formula text proper" reasoning as
# st-lucy-collect.yml, so authored via `propers` rather than a Common
# fallback. No Lectio7-9 override in her own file (defers to Commune/
# C11's own Nocturn 3, same as our-lady-of-the-snows.yml's — see that
# file's own Nocturn 3 comment for why it's duplicated rather than
# 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"
+16
View File
@@ -0,0 +1,16 @@
# Read directly from the reference engine (`../divinum-officium-reference`,
# per memory vu-reference-engine-file-layout, not a live query): untagged/
# default [Rank] block of `web/www/horas/Latin/Sancti/02-02.txt` -- "In
# Purificatione Beatæ Mariæ Virginis;;Duplex 2 class;;5.1;;ex C11". Real
# unique [Oratio]/[Ant 1]/Nocturn 2-3 content of her own (not a bare
# Common fallback the way our-lady-of-the-snows.yml was) -- same
# fixed-BVM-feast pattern as that pass, previously excluded under the
# same stale "not modeled" convention this app has now dropped
# per direct instruction (2026-08): import available content regardless
# 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"
@@ -0,0 +1,24 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/05-31.txt` -- "Beatæ Mariæ
# Virginis Reginæ;;Duplex II classis;;5.1;;ex C11" (Common of Feasts
# of the BVM). Instituted 1954 by Pius XII's encyclical "Ad cæli
# Reginam" -- her own file has full, real, proper content throughout:
# own [Oratio], own [Ant 1] (not a Common substitution, unlike
# presentation-of-the-bvm.yml), and a full nine-lesson Matins office of
# her own. Nocturn 1 (Sir 24, plain scripture) isn't modeled, per this
# app's established convention (getNocturnReadings only ever carries
# Nocturn 2/3 across the whole calendar). Nocturn 2 (Lectio4-6): a
# three-part patristic/magisterial anthology (Peter Canisius,
# Athanasius via Canisius, Pius XII's "Ad cæli Reginam" itself) rather
# than one continuous narrative -- still combined into a single
# reading, matching how this app already treats other multi-source
# Nocturn 2 content. Nocturn 3: her own Gospel citation ends "Et
# réliqua"/"And so on" with no full pericope given -- per the
# bare-incipit convention, only the homily (Lectio7-9, St. Bonaventure)
# 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"
@@ -0,0 +1,15 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/09-15.txt` -- "Septem Dolorum
# Beatæ Mariæ Virginis;;Duplex 2nd class;;5.1;;ex C11". Own real
# [Oratio] and own [Ant 1]. Nocturn 2 (Lectio4-6) is a direct
# `@Tempora/Quad5-5` cross-reference (Friday of Passion Week's own
# temporal content, reused wholesale here) -- not this feast's own
# content, not transcribed, per the bare-cross-reference convention.
# Nocturn 3 (Lectio7-9) is her own real content (St. Ambrose's homily
# 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,12 @@
# Simplex. Own proper collect, no unique antiphon of their own. # Simplex. Own proper collect, no unique antiphon of their own.
id: ss-abdon-and-sennen id: ss-abdon-and-sennen
name: "Ss. Abdon and Sennen, Martyrs" name: "Ss. Abdon and Sennen, Martyrs"
nameLa: "Sancti Abdon et Sennes, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-abdon-and-sennen" propers: "ss-abdon-and-sennen"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (07-30.txt) --
# [Rule]/[Rank] point to Commune/C3 instead, the same C-number family already
# live-query-verified for common-of-several-martyrs.yml (see TODO.md's 'minor
# hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -9,6 +9,12 @@
# caveat as every other stub. No text pulled at all yet. # caveat as every other stub. No text pulled at all yet.
id: ss-alexander-eventius-theodulus-and-juvenal id: ss-alexander-eventius-theodulus-and-juvenal
name: "Ss. Alexander I, Eventius, and Theodulus, Martyrs, and Juvenal, Bishop and Confessor" 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 rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: null propers: "ss-alexander-eventius-theodulus-and-juvenal"
# No [Ant 1] of its own in the source (05-03r.txt) -- [Rule]/[Rank] point to
# Commune/C3 instead, the same C-number family already live-query-verified
# for common-of-several-martyrs.yml (see that section of TODO.md).
minorHoursCommon: common-of-several-martyrs
benedictusCommon: common-of-several-martyrs
@@ -3,6 +3,12 @@
# proper collect and antiphon. # proper collect and antiphon.
id: ss-basilides-cyrinus-nabor-and-nazarius id: ss-basilides-cyrinus-nabor-and-nazarius
name: "Ss. Basilides, Cyrinus, Nabor, and Nazarius, Martyrs" name: "Ss. Basilides, Cyrinus, Nabor, and Nazarius, Martyrs"
nameLa: "Sancti Basilides, Cyrinus, Nabor et Nazarius, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-basilides-cyrinus-nabor-and-nazarius" propers: "ss-basilides-cyrinus-nabor-and-nazarius"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (06-12o.txt) --
# [Rule]/[Rank] point to Commune/C3 instead, the same C-number family already
# live-query-verified for common-of-several-martyrs.yml (see TODO.md's 'minor
# hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -7,6 +7,12 @@
# proper collect, no unique antiphon of their own. # proper collect, no unique antiphon of their own.
id: ss-chrysanthus-and-daria id: ss-chrysanthus-and-daria
name: "Ss. Chrysanthus and Daria, Martyrs" name: "Ss. Chrysanthus and Daria, Martyrs"
nameLa: "Sancti Chrysanthus et Daria, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-chrysanthus-and-daria" propers: "ss-chrysanthus-and-daria"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (10-25.txt) --
# [Rule]/[Rank] point to Commune/C3 instead, the same C-number family already
# live-query-verified for common-of-several-martyrs.yml (see TODO.md's 'minor
# hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -3,6 +3,13 @@
# No unique Oratio/Antiphon -- Common of Several Martyrs only. # No unique Oratio/Antiphon -- Common of Several Martyrs only.
id: ss-cletus-and-marcellinus id: ss-cletus-and-marcellinus
name: "Ss. Cletus and Marcellinus, Popes and Martyrs" name: "Ss. Cletus and Marcellinus, Popes and Martyrs"
nameLa: "Sancti Cletus et Marcellinus, Papæ et Martyres"
rank: semiduplex rank: semiduplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: null propers: null
collectCommon: collect-c3b
collectName:
la: ["Cletum", "Marcellínum"]
en: ["Cletus", "Marcellinus"]
minorHoursCommon: common-of-pope-martyrs
benedictusCommon: common-of-pope-martyrs
@@ -8,6 +8,13 @@
# raw file at this MM-DD slot (Sancti/09-16.txt) is empty. # raw file at this MM-DD slot (Sancti/09-16.txt) is empty.
id: ss-cornelius-and-cyprian id: ss-cornelius-and-cyprian
name: "Ss. Cornelius, Pope, and Cyprian, Bishop, Martyrs" name: "Ss. Cornelius, Pope, and Cyprian, Bishop, Martyrs"
nameLa: "Sanctus Cornelius, Papa, et Sanctus Cyprianus, Episcopus, Martyres"
rank: semiduplex rank: semiduplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: null propers: null
collectCommon: collect-c3
collectName:
la: ["Cornélii", "Cypriáni"]
en: ["Cornelius", "Cyprian"]
minorHoursCommon: common-of-several-martyrs
benedictusCommon: common-of-several-martyrs
@@ -3,6 +3,12 @@
# proper collect, no unique antiphon of their own. # proper collect, no unique antiphon of their own.
id: ss-cosmas-and-damian id: ss-cosmas-and-damian
name: "Ss. Cosmas and Damian, Martyrs" name: "Ss. Cosmas and Damian, Martyrs"
nameLa: "Sancti Cosmas et Damianus, Martyres"
rank: semiduplex rank: semiduplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-cosmas-and-damian" propers: "ss-cosmas-and-damian"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (09-27.txt) --
# [Rule]/[Rank] point to Commune/C3a instead, the same C-number family
# already live-query-verified for common-of-several-martyrs.yml (see
# TODO.md's 'minor hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -6,6 +6,12 @@
# Justina instead, a distinct legend. # Justina instead, a distinct legend.
id: ss-cyprian-and-justina id: ss-cyprian-and-justina
name: "Ss. Cyprian and Justina, Martyrs" name: "Ss. Cyprian and Justina, Martyrs"
nameLa: "Sanctus Cyprianus et Sancta Justina, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-cyprian-and-justina" propers: "ss-cyprian-and-justina"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (09-26.txt) --
# [Rule]/[Rank] point to Commune/C3a-1 instead, the same C-number family
# already live-query-verified for common-of-several-martyrs.yml (see
# TODO.md's 'minor hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -3,6 +3,12 @@
# proper collect, no unique antiphon of their own. # proper collect, no unique antiphon of their own.
id: ss-cyriacus-largus-and-smaragdus id: ss-cyriacus-largus-and-smaragdus
name: "Ss. Cyriacus, Largus, and Smaragdus, Martyrs" name: "Ss. Cyriacus, Largus, and Smaragdus, Martyrs"
nameLa: "Sancti Cyriacus, Largus et Smaragdus, Martyres"
rank: semiduplex rank: semiduplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-cyriacus-largus-and-smaragdus" propers: "ss-cyriacus-largus-and-smaragdus"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (08-08.txt) --
# [Rule]/[Rank] point to Commune/C3a-1 instead, the same C-number family
# already live-query-verified for common-of-several-martyrs.yml (see
# TODO.md's 'minor hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -0,0 +1,18 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/07-07.txt` -- "Duplex;;3;;vide
# C4-1" (a variant Oratio of the Common of a Confessor Bishop, C4 --
# same non-distinct-category reasoning as st-augustine-of-canterbury.yml).
# Own real proper [Oratio] and own Nocturn 2 content (Leo XIII's own
# encyclical letter). No [Ant 1] of its own -- benedictusCommon/
# minorHoursCommon fall back to the plain Common of a Confessor
# Bishop. Nocturn 3 is a direct `@Commune/C1a:LectioN` cross-reference
# (Common of Apostles variant), not this saint's own content -- not
# 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"
benedictusCommon: common-of-a-confessor-bishop
minorHoursCommon: common-of-a-confessor-bishop
@@ -7,6 +7,19 @@
# is correct. # is correct.
id: ss-dionysius-and-companions id: ss-dionysius-and-companions
name: "St. Dionysius, Bishop, and Companions, Martyrs" name: "St. Dionysius, Bishop, and Companions, Martyrs"
nameLa: "Sanctus Dionysius, Episcopus, et Socii, Martyres"
rank: semiduplex rank: semiduplex
common: common-of-a-martyr-bishop common: common-of-a-martyr-bishop
propers: null propers: "ss-dionysius-and-companions"
# Benedictus antiphon live-verified (2026-08, Lauds query, 2025-10-09,
# winner outright) as byte-identical to common-of-several-martyrs's own
# ("Vestri capílli cápitis...") -- despite `common` above saying
# martyr-*bishop*, not several-martyrs; the two fields aren't always the
# same category (see calendar/feasts.ts's SaintRecord.benedictusCommon
# doc comment).
benedictusCommon: common-of-several-martyrs
# P/T/S/N: no [Ant Prima]/etc of its own in the source (10-09t.txt) --
# [Rule]/[Rank] point to Commune/C3a instead, the same C-number family
# already live-query-verified for common-of-several-martyrs.yml (see
# TODO.md's 'minor hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -10,6 +10,12 @@
# placeholders. # placeholders.
id: ss-euphemia-lucia-and-geminianus id: ss-euphemia-lucia-and-geminianus
name: "Ss. Euphemia, Lucy, and Geminianus, Martyrs" name: "Ss. Euphemia, Lucy, and Geminianus, Martyrs"
nameLa: "Sanctæ Euphemia et Lucia, et Sanctus Geminianus, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: null propers: "ss-euphemia-lucia-and-geminianus"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (09-16.txt) --
# [Rule]/[Rank] point to Commune/C3 instead, the same C-number family already
# live-query-verified for common-of-several-martyrs.yml (see TODO.md's 'minor
# hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -3,6 +3,12 @@
# shared by two martyrs. Own proper collect, no unique antiphon of their own. # shared by two martyrs. Own proper collect, no unique antiphon of their own.
id: ss-fabian-and-sebastian id: ss-fabian-and-sebastian
name: "Ss. Fabian and Sebastian, Martyrs" name: "Ss. Fabian and Sebastian, Martyrs"
nameLa: "Sancti Fabianus et Sebastianus, Martyres"
rank: duplex rank: duplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-fabian-and-sebastian" propers: "ss-fabian-and-sebastian"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (01-20.txt) --
# [Rule]/[Rank] point to Commune/C3 instead, the same C-number family already
# live-query-verified for common-of-several-martyrs.yml (see TODO.md's 'minor
# hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -4,6 +4,13 @@
# Oratio/Antiphon in the source -- Common of Several Martyrs only. # Oratio/Antiphon in the source -- Common of Several Martyrs only.
id: ss-faustinus-and-jovita id: ss-faustinus-and-jovita
name: "Ss. Faustinus and Jovita, Martyrs" name: "Ss. Faustinus and Jovita, Martyrs"
nameLa: "Sancti Faustinus et Jovita, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: null propers: null
collectCommon: collect-c3a-1
collectName:
la: ["Faustíni", "Jovítæ"]
en: ["Faustinus", "Jovita"]
minorHoursCommon: common-of-several-martyrs
benedictusCommon: common-of-several-martyrs
@@ -1,16 +1,30 @@
# Verified against Divinum Officium (Monastic Tridentinum 1617), live # Verified against Divinum Officium (Monastic Tridentinum 1617), live
# kalendar check on a clean weekday (2027-08-30, a Monday) -- wins outright # 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 # at Simplex against a bare weekday. On a Saturday this date is instead
# "Sanctae Mariae Sabbato" votive (confirmed re-checking 2025-08-30, a # outranked by the generic "Sanctae Mariae Sabbato" votive (confirmed
# Saturday, where the votive won instead) -- an artifact of the tested # re-checking 2025-08-30, a Saturday, where the votive won instead) -- an
# weekday, same pattern as St. Stephen I (Aug 2) this month and Ss. # artifact of the tested weekday, same pattern as St. Stephen I (Aug 2)
# Marcellinus/Peter/Erasmus in June. No unique collect: the raw file at # this month and Ss. Marcellinus/Peter/Erasmus in June. No unique collect
# this MM-DD slot (Sancti/08-30.txt) is actually St. Rose of Lima's (a # of its own here on the Monastic 1617 track (a live prayTertia query for
# much later, 17th-century addition to this filename slot) -- a live # that track returns no Oratio at all): the raw file at this MM-DD slot
# prayTertia query for the Monastic 1617 track returns no Oratio at all, # (Sancti/08-30.txt) actually belongs to St. Rose of Lima, a much later
# confirming propers: null is correct. # (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 id: ss-felix-and-adauctus
name: "Ss. Felix and Adauctus, Martyrs" name: "Ss. Felix and Adauctus, Martyrs"
nameLa: "Sancti Felix et Adauctus, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: null propers: "ss-felix-and-adauctus"
# Benedictus antiphon live-verified (2026-08, Lauds query, 2025-08-30, winner
# outright) as byte-identical to common-of-several-martyrs.yml's own.
benedictusCommon: common-of-several-martyrs
# P/T/S/N: no [Ant Prima]/etc of its own in the source (08-30o.txt) --
# [Rule]/[Rank] point to Commune/C3a instead, the same C-number family
# already live-query-verified for common-of-several-martyrs.yml (see
# TODO.md's 'minor hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -9,6 +9,12 @@
# text pulled at all yet. # text pulled at all yet.
id: ss-felix-simplicius-faustinus-and-beatrix id: ss-felix-simplicius-faustinus-and-beatrix
name: "Ss. Felix, Simplicius, Faustinus, and Beatrix, Martyrs" name: "Ss. Felix, Simplicius, Faustinus, and Beatrix, Martyrs"
nameLa: "Sancti Felix, Simplicius, Faustinus et Beatrix, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: null propers: "ss-felix-simplicius-faustinus-and-beatrix"
# No [Ant 1] of its own in the source (07-29cc.txt) -- [Rule]/[Rank] point to
# Commune/C3a instead, the same C-number family already live-query-verified
# for common-of-several-martyrs.yml (see that section of TODO.md).
minorHoursCommon: common-of-several-martyrs
benedictusCommon: common-of-several-martyrs
@@ -3,6 +3,12 @@
# proper collect, no unique antiphon of their own. # proper collect, no unique antiphon of their own.
id: ss-gervase-and-protase id: ss-gervase-and-protase
name: "Ss. Gervase and Protase, Martyrs" name: "Ss. Gervase and Protase, Martyrs"
nameLa: "Sancti Gervasius et Protasius, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-gervase-and-protase" propers: "ss-gervase-and-protase"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (06-19o.txt) --
# [Rule]/[Rank] point to Commune/C3a-1 instead, the same C-number family
# already live-query-verified for common-of-several-martyrs.yml (see
# TODO.md's 'minor hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -0,0 +1,13 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/08-13.txt` -- "Simplex;;1.1;;
# vide C3a-1" (Common of Several Martyrs). Own real proper [Oratio].
# Single simplex-feast lesson (Lectio93), one continuous account
# covering both martyrs, same pattern as
# 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"
minorHoursCommon: common-of-several-martyrs
@@ -5,6 +5,12 @@
# antiphon. # antiphon.
id: ss-john-and-paul id: ss-john-and-paul
name: "Ss. John and Paul, Martyrs" name: "Ss. John and Paul, Martyrs"
nameLa: "Sancti Joannes et Paulus, Martyres"
rank: semiduplex rank: semiduplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-john-and-paul" propers: "ss-john-and-paul"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (06-26.txt) --
# [Rule]/[Rank] point to Commune/C3 instead, the same C-number family already
# live-query-verified for common-of-several-martyrs.yml (see TODO.md's 'minor
# hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
+7 -1
View File
@@ -4,6 +4,12 @@
# an artifact of the tested year. rank/propers unverified placeholders. # an artifact of the tested year. rank/propers unverified placeholders.
id: ss-machabees id: ss-machabees
name: "Ss. Machabees, Martyrs" name: "Ss. Machabees, Martyrs"
nameLa: "Sancti Machabæi, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: null propers: "ss-machabees"
# No [Ant 1] of its own in the source (08-01r.txt) -- [Rule]/[Rank] point to
# Commune/C3 instead, the same C-number family already live-query-verified
# for common-of-several-martyrs.yml (see that section of TODO.md).
minorHoursCommon: common-of-several-martyrs
benedictusCommon: common-of-several-martyrs
@@ -5,6 +5,12 @@
# their own. # their own.
id: ss-marcellinus-peter-and-erasmus id: ss-marcellinus-peter-and-erasmus
name: "Ss. Marcellinus, Peter, and Erasmus, Bishop, Martyrs" name: "Ss. Marcellinus, Peter, and Erasmus, Bishop, Martyrs"
nameLa: "Sancti Marcellinus et Petrus, et Sanctus Erasmus, Episcopus, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-marcellinus-peter-and-erasmus" propers: "ss-marcellinus-peter-and-erasmus"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (06-02.txt) --
# [Rule]/[Rank] point to Commune/C3 instead, the same C-number family already
# live-query-verified for common-of-several-martyrs.yml (see TODO.md's 'minor
# hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -7,6 +7,12 @@
# author" scope for merely-commemorated saints. # author" scope for merely-commemorated saints.
id: ss-marius-martha-audifax-and-abachum id: ss-marius-martha-audifax-and-abachum
name: "Ss. Marius, Martha, Audifax, and Abachum, Martyrs" name: "Ss. Marius, Martha, Audifax, and Abachum, Martyrs"
nameLa: "Sancti Marius, Martha, Audifax et Abachum, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: null propers: "ss-marius-martha-audifax-and-abachum"
# No [Ant 1] of its own in the source (01-19.txt) -- [Rule]/[Rank] point to
# Commune/C3a instead, the same C-number family already live-query-verified
# for common-of-several-martyrs.yml (see that section of TODO.md).
minorHoursCommon: common-of-several-martyrs
benedictusCommon: common-of-several-martyrs
@@ -3,6 +3,12 @@
# proper collect, no unique antiphon of their own. # proper collect, no unique antiphon of their own.
id: ss-mark-and-marcellian id: ss-mark-and-marcellian
name: "Ss. Mark and Marcellian, Martyrs" name: "Ss. Mark and Marcellian, Martyrs"
nameLa: "Sancti Marcus et Marcellianus, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-mark-and-marcellian" propers: "ss-mark-and-marcellian"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (06-18t.txt) --
# [Rule]/[Rank] point to Commune/C3a-1 instead, the same C-number family
# already live-query-verified for common-of-several-martyrs.yml (see
# TODO.md's 'minor hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -8,6 +8,15 @@
# August's entries. # August's entries.
id: ss-maurice-and-companions id: ss-maurice-and-companions
name: "Ss. Maurice and Companions, Martyrs" name: "Ss. Maurice and Companions, Martyrs"
nameLa: "Sanctus Mauritius et Socii, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: null propers: "ss-maurice-and-companions"
# Benedictus antiphon live-verified (2026-08, Lauds query, 2025-09-22, winner
# outright) as byte-identical to common-of-several-martyrs.yml's own.
benedictusCommon: common-of-several-martyrs
# P/T/S/N: no [Ant Prima]/etc of its own in the source (09-22o.txt) --
# [Rule]/[Rank] point to Commune/C3 instead, the same C-number family already
# live-query-verified for common-of-several-martyrs.yml (see TODO.md's 'minor
# hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -9,6 +9,12 @@
# text pulled at all yet. # text pulled at all yet.
id: ss-nabor-and-felix id: ss-nabor-and-felix
name: "Ss. Nabor and Felix, Martyrs" name: "Ss. Nabor and Felix, Martyrs"
nameLa: "Sancti Nabor et Felix, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: null propers: "ss-nabor-and-felix"
# No [Ant 1] of its own in the source (07-12o.txt) -- [Rule]/[Rank] point to
# Commune/C3a-1 instead, the same C-number family already live-query-verified
# for common-of-several-martyrs.yml (see that section of TODO.md).
minorHoursCommon: common-of-several-martyrs
benedictusCommon: common-of-several-martyrs
@@ -6,6 +6,12 @@
# antiphon of their own. # antiphon of their own.
id: ss-nazarius-celsus-victor-and-innocent 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" 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 rank: semiduplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-nazarius-celsus-victor-and-innocent" propers: "ss-nazarius-celsus-victor-and-innocent"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (07-28.txt) --
# [Rule]/[Rank] point to Commune/C3 instead, the same C-number family already
# live-query-verified for common-of-several-martyrs.yml (see TODO.md's 'minor
# hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -3,6 +3,12 @@
# Own proper collect, no unique antiphon of their own. # Own proper collect, no unique antiphon of their own.
id: ss-nereus-achilleus-domitilla-and-pancras id: ss-nereus-achilleus-domitilla-and-pancras
name: "Ss. Nereus, Achilleus, Domitilla, and Pancras, Martyrs" name: "Ss. Nereus, Achilleus, Domitilla, and Pancras, Martyrs"
nameLa: "Sancti Nereus, Achilleus et Pancratius, et Sancta Domitilla, Martyres"
rank: semiduplex rank: semiduplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-nereus-achilleus-domitilla-and-pancras" propers: "ss-nereus-achilleus-domitilla-and-pancras"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (05-12.txt) --
# [Rule]/[Rank] point to Commune/C3a-1 instead, the same C-number family
# already live-query-verified for common-of-several-martyrs.yml (see
# TODO.md's 'minor hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -0,0 +1,16 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/03-06.txt` -- "vide C7b;;Duplex;;3;;".
# No [Oratio]/[Ant 1] of her own -- both fall back to Common of several
# women Martyrs (C7b), newly authored here (collect-c7b.yml,
# 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
collectCommon: collect-c7b
collectName:
la: ["Perpétuæ", "Felicitátis"]
en: ["Perpetua", "Felicitas"]
benedictusCommon: common-of-several-women-martyrs
@@ -5,6 +5,7 @@
# duplicated here rather than shared, see ss-peter-and-paul-antiphon.yml. # duplicated here rather than shared, see ss-peter-and-paul-antiphon.yml.
id: ss-peter-and-paul id: ss-peter-and-paul
name: "Ss. Peter and Paul, Apostles" name: "Ss. Peter and Paul, Apostles"
nameLa: "Sancti Petrus et Paulus, Apostoli"
rank: duplex-1-classis rank: duplex-1-classis
common: common-of-an-apostle common: common-of-an-apostle
propers: "ss-peter-and-paul" propers: "ss-peter-and-paul"
@@ -3,6 +3,8 @@
# classis. Own proper collect and antiphon. # classis. Own proper collect and antiphon.
id: ss-philip-and-james id: ss-philip-and-james
name: "Ss. Philip and James, Apostles" name: "Ss. Philip and James, Apostles"
nameLa: "Sancti Philippus et Jacobus, Apostoli"
rank: duplex-2-classis rank: duplex-2-classis
common: common-of-an-apostle common: common-of-an-apostle
propers: "ss-philip-and-james" propers: "ss-philip-and-james"
minorHoursCommon: common-of-apostles
@@ -10,6 +10,13 @@
# treatment as any other saint whose own file just points at the Common. # treatment as any other saint whose own file just points at the Common.
id: ss-placid-and-companions id: ss-placid-and-companions
name: "Ss. Placid and Companions, Martyrs" name: "Ss. Placid and Companions, Martyrs"
nameLa: "Sanctus Placidus et Socii, Martyres"
rank: duplex-2-classis rank: duplex-2-classis
common: common-of-several-martyrs common: common-of-several-martyrs
propers: null propers: null
collectCommon: collect-c3a
collectName:
la: ["Plácidi", "Sociórum ejus"]
en: ["Placidus", "his companions"]
minorHoursCommon: common-of-several-martyrs
benedictusCommon: common-of-several-martyrs
@@ -5,6 +5,12 @@
# antiphon of their own. # antiphon of their own.
id: ss-primus-and-felicianus id: ss-primus-and-felicianus
name: "Ss. Primus and Felicianus, Martyrs" name: "Ss. Primus and Felicianus, Martyrs"
nameLa: "Sancti Primus et Felicianus, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-primus-and-felicianus" propers: "ss-primus-and-felicianus"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (06-09.txt) --
# [Rule]/[Rank] point to Commune/C3a instead, the same C-number family
# already live-query-verified for common-of-several-martyrs.yml (see
# TODO.md's 'minor hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -0,0 +1,13 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/09-11.txt` -- "Simplex;;1.1;;
# vide C3a-1" (Common of Several Martyrs). Own real proper [Oratio].
# Single simplex-feast lesson (Lectio93), one continuous account
# covering both martyrs, same pattern as
# 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"
minorHoursCommon: common-of-several-martyrs
@@ -3,6 +3,12 @@
# Own proper collect, no unique antiphon of their own. # Own proper collect, no unique antiphon of their own.
id: ss-seven-brothers-rufina-and-secunda id: ss-seven-brothers-rufina-and-secunda
name: "Ss. Seven Brothers, Martyrs, and Ss. Rufina and Secunda, Virgins and Martyrs" 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 rank: semiduplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-seven-brothers-rufina-and-secunda" propers: "ss-seven-brothers-rufina-and-secunda"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (07-10.txt) --
# [Rule]/[Rank] point to Commune/C3 instead, the same C-number family already
# live-query-verified for common-of-several-martyrs.yml (see TODO.md's 'minor
# hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -0,0 +1,12 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/02-12.txt` -- "vide C5c;;Duplex;;3;;".
# Own proper [Oratio]/[Ant 1]/Nocturn 2 vita; Nocturn 3 is a direct
# `@Commune/C1:LectioN` cross-reference to the Common of Apostles' own
# Gospel-and-homily (Matt 19:27-29, "we have forsaken all"), fitting for
# 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. # alone. Own proper collect, no unique antiphon of their own.
id: ss-simon-and-jude id: ss-simon-and-jude
name: "Ss. Simon and Jude, Apostles" name: "Ss. Simon and Jude, Apostles"
nameLa: "Sancti Simon et Judas, Apostoli"
rank: duplex-2-classis rank: duplex-2-classis
common: common-of-an-apostle common: common-of-an-apostle
propers: "ss-simon-and-jude" propers: "ss-simon-and-jude"
@@ -6,6 +6,13 @@
# this set. No unique Oratio/Antiphon -- Common of Several Martyrs only. # this set. No unique Oratio/Antiphon -- Common of Several Martyrs only.
id: ss-soter-and-caius id: ss-soter-and-caius
name: "Ss. Soter and Caius, Popes and Martyrs" name: "Ss. Soter and Caius, Popes and Martyrs"
nameLa: "Sancti Soter et Cajus, Papæ et Martyres"
rank: semiduplex rank: semiduplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: null propers: null
collectCommon: collect-c3b
collectName:
la: ["Sotérem", "Cajum"]
en: ["Soter", "Caius"]
minorHoursCommon: common-of-pope-martyrs
benedictusCommon: common-of-pope-martyrs
@@ -3,6 +3,12 @@
# proper collect, no unique antiphon of their own. # proper collect, no unique antiphon of their own.
id: ss-symphorosa-and-seven-sons id: ss-symphorosa-and-seven-sons
name: "Ss. Symphorosa and Her Seven Sons, Martyrs" name: "Ss. Symphorosa and Her Seven Sons, Martyrs"
nameLa: "Sancta Symphorosa et Septem Filii Ejus, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-symphorosa-and-seven-sons" propers: "ss-symphorosa-and-seven-sons"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (07-18o.txt) --
# [Rule]/[Rank] point to Commune/C3a instead, the same C-number family
# already live-query-verified for common-of-several-martyrs.yml (see
# TODO.md's 'minor hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -0,0 +1,13 @@
# Read directly from the reference engine: untagged/default [Rank]
# block of `web/www/horas/Latin/Sancti/08-11.txt` -- "Simplex;;1.1;;
# vide C3a-1" (Common of Several Martyrs). Own real proper [Oratio].
# Single simplex-feast lesson (Lectio93), one continuous account
# covering both martyrs, same pattern as
# 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"
minorHoursCommon: common-of-several-martyrs
@@ -4,6 +4,12 @@
# their own. # their own.
id: ss-tiburtius-valerian-and-maximus id: ss-tiburtius-valerian-and-maximus
name: "Ss. Tiburtius, Valerian, and Maximus, Martyrs" name: "Ss. Tiburtius, Valerian, and Maximus, Martyrs"
nameLa: "Sancti Tiburtius, Valerianus et Maximus, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-tiburtius-valerian-and-maximus" propers: "ss-tiburtius-valerian-and-maximus"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (04-14t.txt) --
# [Rule]/[Rank] point to Commune/C3a instead, the same C-number family
# already live-query-verified for common-of-several-martyrs.yml (see
# TODO.md's 'minor hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -6,6 +6,15 @@
# confirming propers: null is correct. # confirming propers: null is correct.
id: ss-tryphon-and-companions id: ss-tryphon-and-companions
name: "Ss. Tryphon, Respicius, and Nympha, Martyrs" name: "Ss. Tryphon, Respicius, and Nympha, Martyrs"
nameLa: "Sancti Tryphon, Respicius et Nympha, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: null propers: "ss-tryphon-and-companions"
# Benedictus antiphon live-verified (2026-08, Lauds query, 2025-11-10, winner
# outright) as byte-identical to common-of-several-martyrs.yml's own.
benedictusCommon: common-of-several-martyrs
# P/T/S/N: no [Ant Prima]/etc of its own in the source (11-10o.txt) --
# [Rule]/[Rank] point to Commune/C3a instead, the same C-number family
# already live-query-verified for common-of-several-martyrs.yml (see
# TODO.md's 'minor hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -4,6 +4,12 @@
# tested year. rank/propers unverified placeholders. # tested year. rank/propers unverified placeholders.
id: ss-twelve-brothers id: ss-twelve-brothers
name: "Ss. Twelve Brothers, Martyrs" name: "Ss. Twelve Brothers, Martyrs"
nameLa: "Duodecim Fratres, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: null propers: "ss-twelve-brothers"
# No [Ant 1] of its own in the source (09-01cc.txt) -- [Rule]/[Rank] point to
# Commune/C3 instead, the same C-number family already live-query-verified
# for common-of-several-martyrs.yml (see that section of TODO.md).
minorHoursCommon: common-of-several-martyrs
benedictusCommon: common-of-several-martyrs
@@ -2,8 +2,24 @@
# commemorated under St. Hilarion (see st-hilarion.yml), never wins # commemorated under St. Hilarion (see st-hilarion.yml), never wins
# outright; both are fixed, non-movable dates, so this isn't an artifact # outright; both are fixed, non-movable dates, so this isn't an artifact
# of the tested year. rank/propers unverified placeholders. # of the tested year. rank/propers unverified placeholders.
#
# No minorHoursCommon/benedictusCommon: her collect template (collect-c6b,
# plural Virgins and Martyrs) has no other member on this app's calendar
# to cross-verify a shared P/T/S/N/Benedictus text against, and she can
# never be queried directly herself (never the day's own winner) -- rather
# than guess she shares common-of-a-virgin's singular text (2026-08's
# live-verified category, built from St. Justina/St. Emerentiana/
# St. Praxedes/St. Martha, all singular), left unset, same "honestly
# missing" convention as every other unauthored fallback in this codebase.
# Moot in practice: a commemoration's minor hours are never rendered
# anyway (resolveOfficeWinner only ever returns the day's actual winner).
id: ss-ursula-and-companions id: ss-ursula-and-companions
name: "Ss. Ursula and Companions, Virgins and Martyrs" name: "Ss. Ursula and Companions, Virgins and Martyrs"
nameLa: "Sancta Ursula et Sociæ, Virgines et Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: null propers: null
collectCommon: collect-c6b
collectName:
la: ["Ursulæ", "Sociárum"]
en: ["Ursula", "her companions"]
@@ -3,6 +3,12 @@
# proper collect, no unique antiphon of their own. # proper collect, no unique antiphon of their own.
id: ss-vincent-and-anastasius id: ss-vincent-and-anastasius
name: "Ss. Vincent and Anastasius, Martyrs" name: "Ss. Vincent and Anastasius, Martyrs"
nameLa: "Sancti Vincentius et Anastasius, Martyres"
rank: semiduplex rank: semiduplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-vincent-and-anastasius" propers: "ss-vincent-and-anastasius"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (01-22.txt) --
# [Rule]/[Rank] point to Commune/C3 instead, the same C-number family already
# live-query-verified for common-of-several-martyrs.yml (see TODO.md's 'minor
# hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
@@ -3,6 +3,12 @@
# proper collect, no unique antiphon of their own. # proper collect, no unique antiphon of their own.
id: ss-vitus-modestus-and-crescentia id: ss-vitus-modestus-and-crescentia
name: "Ss. Vitus, Modestus, and Crescentia, Martyrs" name: "Ss. Vitus, Modestus, and Crescentia, Martyrs"
nameLa: "Sancti Vitus, Modestus et Crescentia, Martyres"
rank: simplex rank: simplex
common: common-of-several-martyrs common: common-of-several-martyrs
propers: "ss-vitus-modestus-and-crescentia" propers: "ss-vitus-modestus-and-crescentia"
# P/T/S/N: no [Ant Prima]/etc of its own in the source (06-15.txt) --
# [Rule]/[Rank] point to Commune/C3 instead, the same C-number family already
# live-query-verified for common-of-several-martyrs.yml (see TODO.md's 'minor
# hour content gaps' section).
minorHoursCommon: common-of-several-martyrs
+16 -3
View File
@@ -1,10 +1,23 @@
# Verified against Divinum Officium (Monastic Tridentinum 1617), live kalendar # Verified against Divinum Officium (Monastic Tridentinum 1617), live kalendar
# check (2029-02-05, no Sunday collision) -- winner outright, Semiduplex. Own # check (2029-02-05, no Sunday collision) -- winner outright, Semiduplex. Own
# proper antiphon (see propers/common/st-agatha-antiphon.yml) but no unique # proper antiphon (see propers/common/st-agatha-antiphon.yml). No unique
# collect of her own -- Common of a Virgin Martyr supplies that instead, so # collect of her own -- Common of a Virgin Martyr supplies that instead
# getDayCollect resolves honestly missing for her despite propers being set. # (her own [Oratio] is @Commune/C6::s/N\./Agathæ/, a plain name-substituted
# reference, confirmed identical to collect-c6.yml's own template) -- 2026-08,
# wired via collectCommon now that mechanism exists (it postdates this
# saint's own original authoring, hence the gap).
id: st-agatha id: st-agatha
name: "St. Agatha, Virgin and Martyr" name: "St. Agatha, Virgin and Martyr"
nameLa: "Sancta Agatha, Virgo et Martyr"
rank: semiduplex rank: semiduplex
common: common-of-a-virgin-martyr common: common-of-a-virgin-martyr
propers: "st-agatha" propers: "st-agatha"
collectCommon: collect-c6
collectName:
la: ["Agathæ"]
en: ["Agatha"]
# P/T/S/N: no [Ant Prima]/etc of its own in the source (02-05.txt) --
# [Rule]/[Rank] point to Commune/C6 instead, the same C-number family already
# live-query-verified for common-of-a-virgin.yml (see TODO.md's 'minor hour
# content gaps' section).
minorHoursCommon: common-of-a-virgin

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