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.
When more than one octave is active on a day with no temporal standing of
its own (St. Lawrence's and the Assumption's genuinely overlap every Aug
16-17), which one governs the day's content/label is now decided by rank
comparison instead of "whichever started first" (an accident of
insertion order). Per direct instruction:
- Highest effective rank wins outright; every other active octave still
gets commemorated, same as a single octave already outranking a weak
rival saint.
- A rank tie goes to whichever octave started more recently -- day 1 of
a new octave needs to be fully present, the whole point of it
starting. No real tied-rank case exists yet to verify this
empirically, unlike everything else here -- documented in TODO.md as
a stated decision, not a live finding.
New OctaveConfig.closingDayRank (default duplex): an octave's own final
day ("in Octava") is elevated above its ordinary in-between rank --
live-verified as a real, general pattern (both St. Lawrence's Aug 17 and
the Assumption's Aug 22 show as Duplex, above their otherwise-Semiduplex
ordinary days), not a one-off. This elevation is *why* Lawrence's octave
beats the Assumption's on their one real overlap day despite the
Assumption being the far higher-ranked feast overall. Feeds both the
existing rival-saint threshold and the new octave-vs-octave comparison.
calendar/octaves.ts gains resolveActiveOctave (+ pickWinningOctave, the
comparison itself factored out for direct unit testing against synthetic
data, since no real tied-rank overlap exists to test against yet).
hours/resolve-common.ts's resolveOfficeWinner and calendar/day-label.ts
both now call it instead of each keeping their own "activeOctavesFor(...)
[0]" logic.
That consolidation surfaced a real, independent bug: getDayLabel never
checked temporalCategory at all before choosing an octave name, unlike
resolveOfficeWinner -- found while testing the real Aug 16 overlap (a
Sunday that year, where the temporal Sunday has standing and should win
outright). Live-verified counterexample: the Christmas Octave's own
stack (Dec 30) was wrongly labeled "3rd Day within the Octave of The Holy
Innocents" instead of the correct plain temporal label -- the real title
never names any of the four stacked octaves there. Fixed by sharing the
exact same ordinary-feria gate resolveOfficeWinner already had, so the
two can no longer disagree.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Assumption (Aug 15-22), Nativity of the BVM (Sep 8-15), Immaculate
Conception (Dec 8-15), and All Saints (Nov 1-8) now declare octave:
{ enabled: true }, same generic mechanism as St. Lawrence and the
Christmas trio. Every octave this pull has identified is now modeled;
Matins reading content is a separate, larger follow-up (see conversation)
-- these still just commemorate at L/V-equivalent level for now.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds calendar/octaves.ts: a lookback over the past week collecting every
octave (sanctoral or temporal) still active on a date, stacking multiple
at once (Christmas + St. Stephen + St. John + Holy Innocents all
commemorated together within the Christmas Octave). Declared via an
optional `octave` field on a saint's own record or a new small
TemporalFeastRecord (calendar/temporal-feasts.ts) for temporal ids like
Christmas/Pentecost that didn't have a metadata record before -- data-
driven per user design discussion, with `{ enabled: true }` alone using
sensible defaults (8 days, semiduplex threshold) so a minimal declaration
works without authored content.
Wired into resolveDay as a post-processing layer: doesn't change how a
single day's own precedence contest is decided, just adds commemorations
for active octaves and occasionally overrides the winner when the
occurring saint doesn't clear the strictest active octave's threshold.
Populated so far: St. Lawrence's own octave (the one that repeatedly cost
real saints their spot in August), the three Comites Christi octaves
(Stephen/John/Innocents -- Thomas of Canterbury deliberately excluded,
per discussion), and Pentecost's (duplex threshold, user-specified).
Pentecost's octave offsets are also removed from temporal-categories.yml's
privileged-feria-major classification, letting a real candidate reach the
new octave layer instead of being transferred away first -- with the
side effect that Pentecost's own Ember Saturday no longer forces a
transfer (a sub-threshold saint is now commemorated in place instead, see
tests/calendar/transfer.test.ts's updated case). Assumption, Nativity
BVM, Immaculate Conception, and All Saints' own octaves are not yet
populated with octave data -- deliberately deferred to a follow-up pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>