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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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.
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.
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
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
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
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>
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>
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.
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.
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).
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.
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>
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>
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>
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>