Commit Graph

101 Commits

Author SHA1 Message Date
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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