750ccbf6d4
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
2847 lines
193 KiB
Markdown
2847 lines
193 KiB
Markdown
# Open work
|
||
|
||
A real backlog, not scattered code comments. Update this file when something
|
||
here gets done, and add to it when new gaps turn up — don't let it drift back
|
||
into "only findable by reading commit messages."
|
||
|
||
## Priority
|
||
|
||
Work in this file follows one methodology, repeated every time a new hour or content type
|
||
gets built: **mechanism first, then a minimal live-verified proof (1-2 clean dates/entries
|
||
authored fully, end-to-end) to confirm the mechanism is right, then bulk content
|
||
authoring/import against the whole calendar once it's trusted.** A section marked "mechanism
|
||
built" but not yet "full sweep" or "all N done" is mid-pipeline, not stalled — its next work
|
||
is more content in the same already-proven shape, not a design change. This file's dated
|
||
build log (below "Open work, in priority order") is the record of that pipeline running
|
||
across every hour and content type in this app.
|
||
|
||
## Open work, in priority order
|
||
|
||
1. **Mechanism gaps** — a real design/code piece not built yet, blocking any content pass on
|
||
it from starting:
|
||
- Easter's own octave — tabled, "a whole different discussion."
|
||
- What "a Gospel reading is its own kind" should mean structurally/in the UI, beyond the
|
||
`isGospel` flag on a plain Matins `lesson` — open; deferred until there's a UI to
|
||
evaluate it against.
|
||
- Date picker: `day-nav.ts` currently only steps a day at a time (prev/next); there's no
|
||
way to jump to an arbitrary date without walking there one day at a time or editing the
|
||
URL. Needs a calendar-grid picker UI — open, not started.
|
||
- Audio ("read it to me") interface: a button per hour/text that plays the office aloud
|
||
instead of requiring reading — e.g. via the Web Speech API (`SpeechSynthesis`), which
|
||
would keep this offline-first (no server-side TTS call, no added deploy dependency) but
|
||
needs bilingual voice selection (La/En) worked out, and quality for Latin pronunciation
|
||
is an open question. Open, not started — needs its own design pass before content/UI work
|
||
begins.
|
||
- ~~Matins: "Te decet laus"~~ — done (2026-08-23). Added as a new common text
|
||
(`data/propers/common/te-decet-laus.yml`, sourced from `Psalterium/Common/Prayers.txt`'s
|
||
`[Te decet]` block) rendered as a `versicle` part immediately after the Te Deum on every
|
||
3-nocturn day (Sunday or Duplex+), in both `hours/matins.ts` branches that push a Te Deum.
|
||
Not modeled: the historical Gospel-read-again-in-full step that live-precedes it (a
|
||
pre-existing, separate known simplification — see
|
||
`data/propers/nocturn-readings/post-pentecost-15.yml`'s own header comment). Absent on
|
||
ferial (1-nocturn) days, same gating as the Te Deum itself. `tests/hours/matins.test.ts`
|
||
covers both the Sunday branch's presence/content and the ferial branch's absence.
|
||
- ~~Matins nocturn-readings responsory formatting~~ — done (2026-08-23), see "Nocturn-
|
||
readings responsory marker fix" below.
|
||
- ~~Lauds/Vespers ferial Preces psalms~~ — done (2026-08-23). Psalm 129 (De profundis) and
|
||
its own trailing Gloria Patri deleted wholesale from
|
||
`data/propers/common/lauds-preces-feriales.yml`'s `text.la`/`text.en`; Psalm 50 (Miserere)
|
||
and its own Gloria Patri likewise from `vespers-preces-feriales.yml`. Surrounding V/R
|
||
litany needed no adjustment — the psalm block sat between two already-independent V/R
|
||
sequences. Both files' header comments updated to flag the drop as deliberate, not a
|
||
transcription gap. `tests/hours/lauds.test.ts` and `vespers.test.ts` updated to
|
||
distinguish the fuller ferial Preces from the short Sunday/feast litany via a versicle
|
||
that survives the psalm's removal ("Ego dixi: Dómine, miserére mei") instead of the now-
|
||
absent psalm text.
|
||
|
||
2. **Minimal-proof-done, bulk content pending** — mechanism is built and trusted (a proof
|
||
date/entry already resolves correctly end-to-end); what's left is authoring/importing more
|
||
content in that same shape across the rest of the calendar:
|
||
- Latin saint/octave/named-feast names for the bilingual date/day-label header (see
|
||
"Bilingual, merged date/day-label header" below) — the `{en, la}` mechanism is built and
|
||
every proper name falls back to English gracefully, but zero saints, octaves, or named
|
||
temporal feasts have an authored Latin name anywhere in `src/data/**`. Weekday names,
|
||
month names, rank labels, and season/ordinal phrasing already have real Latin.
|
||
- Matins: bible-plan TSV bulk conversion is done (see "Bible-plan TSV — bulk conversion
|
||
done" below) — not yet added to a test table, though (unlike sanctoral/temporal content)
|
||
there's no reference breviary to spot-check this plan against by design. Still open:
|
||
author patristic/hagiographic
|
||
readings for every saint where one can be sourced — no longer Duplex+-gated, content is
|
||
pooled and shown regardless of rank (see "Reading-pool redesign"); expand the per-book
|
||
Matins responsory pool beyond its one seeded book (`isa`). The pre-1955-but-post-1617
|
||
saints (Tridentine/Divino Afflatu-sourced, no Monastic 1617 track) are now done — see
|
||
"Pre-1955 saints' Matins readings" below. The larger pool of saints that *do* have a
|
||
Monastic 1617 track (`SanctiM/`) is now also done — see "SanctiM-track saints' Matins
|
||
readings" below — all 12 months complete, 133 saints authored, the rest correctly left
|
||
unauthored as Common-only/no-content cases.
|
||
- Matins: the **temporal-cycle** side of this same gap (as opposed to the sanctoral side
|
||
above) — the post-Pentecost season (`post-pentecost-01` through `-24`) is now fully
|
||
authored (see "Temporal-cycle nocturn-readings sweep" below); the rest of the year's
|
||
temporal ids (Advent, Christmastide, post-Epiphany, Septuagesima through Lent/Passiontide,
|
||
Eastertide) still have no Nocturn 2/3 content of their own — same shape of gap, not yet
|
||
started.
|
||
- The bulk Vulgate (Latin) + Douay-Rheims (English) scripture import into
|
||
`src/data/scripture/` — long-term project, explicitly deferred; Matins scripture
|
||
citations resolve to `missing` text until it lands.
|
||
- Matins psalmody overrides for a Duplex+ weekday feast (see "Matins psalmody overrides"
|
||
below): mechanism refactored to category-keyed (`categories/*.yml` + `saints/*.yml`), five
|
||
categories authored (Common of a Martyr via St. Lawrence, Common of an Apostle via St.
|
||
Andrew, Common of a Confessor via St. Francis of Paola, Common of a Confessor-Bishop via
|
||
St. Basil the Great, Common of Several Martyrs — the largest single category by population,
|
||
40 saints/groups — via Ss. Fabian and Sebastian). Every other Common category (Virgin,
|
||
Doctor, Abbot, etc.) still falls back to the plain ferial weekday table, redistributed into
|
||
3 nocturns, until its own category's real psalmody is authored.
|
||
- The four "Commemoratio Octavæ" Ant+V/R+collect blocks (Christmas Octave's own
|
||
Nativity/Stephen/John/Holy-Innocents commemorations, e.g. on Dec 30) are now done —
|
||
see "Octave content" below. St. Frances of Rome's/
|
||
St. Bibiana's Benedictus antiphons and Ss. Ursula and Companions' were also once flagged
|
||
here — the first two turned out already resolved (verified live, see memory
|
||
`vu-todo-priority`'s own correction note), the third is a settled non-gap (no unique text
|
||
exists to author, see her own file's comment) — not carried forward as open items.
|
||
Five "fixed Lord's/BVM feast, not modeled" exclusions across the calendar (Our Lady of the
|
||
Snows, the Finding of the Holy Cross, the Transfiguration, the Exaltation of the Holy
|
||
Cross, the Dedication of the Lateran, the Presentation of the BVM) turned out to be the
|
||
same stale mistake repeated six times — all six now added with real content; see "Our
|
||
Lady of the Snows added" and "Four more 'not modeled' fixed Lord's/BVM feasts added"
|
||
below.
|
||
|
||
3. **Known, deliberate simplifications** — flagged as intentional, not a bug, no plan to
|
||
revisit unless priorities change: see "Known, deliberate simplifications" at the end of
|
||
this file, plus the Matins "Duo Seraphim" closing-responsory gap and the 5 Vespers hymns
|
||
whose seasonal doxology substitution isn't modeled (both flagged in their own sections
|
||
below, not repeated here).
|
||
|
||
Everything from here down is the dated build log this priority list is drawn from — kept for
|
||
the sourcing/verification detail behind each decision, not itself in priority order.
|
||
|
||
## Content gaps — mechanism built, data incomplete
|
||
|
||
### Lauds psalmody overrides + office-bundle overrides — done (2026-08)
|
||
|
||
Mechanism: `src/hours/lauds-psalmody-overrides.ts`, gated in
|
||
`hours/lauds.ts`'s `getPsalmodyOverrideFor`; office bundle
|
||
(`lauds-capitulum-/-responsory-/-hymn-/-versicle-<id>.yml` in
|
||
`data/propers/common/`) falls back to the plain weekday bundle
|
||
independently of whether the psalmody override exists. Fires for a saint
|
||
ranked duplex-majus or higher, or a named temporal feast
|
||
(`marian-saturday`, `christ-the-king`, `christmas-octave-sunday`).
|
||
|
||
**Authored — all 28 non-`propers: null` duplex-majus+ saints/temporal
|
||
ids, plus the 4 already done going in (`st-lawrence`, `marian-saturday`,
|
||
`christ-the-king`, `christmas-octave-sunday`) — full 32/32.** Same
|
||
methodology as the P/T/S/N batch: one live `command=prayLaudes` query per
|
||
saint's own already-established clean date, Monastic Tridentinum 1617.
|
||
Extraction automated this time via a purpose-built HTML parser
|
||
(`scratchpad/parse_lauds_html.py`, not committed — a one-off tool, not
|
||
application code) after manual grep/sed proved too slow at this volume;
|
||
cross-validated against 5 saints' worth of already-known-correct content
|
||
before trusting it for the rest, then spot-checked further plus an
|
||
automated la==en/HTML-leftover sweep across all 28 outputs. Verified
|
||
end-to-end with a runtime smoke test resolving each id's actual
|
||
`getLaudsPsalmodyOverride` + all four bundle parts through the real
|
||
mechanism, not just checking the YAML files exist.
|
||
|
||
The 5 antiphons cycle through a fixed pool matching the psalm scheme
|
||
every one of these major feasts uses ("Psalmi Dominica"): psalms
|
||
92/99/62, the Canticle of the Three Young Men, then the Laudate group
|
||
(Ps 148-150, one shared antiphon) — confirmed via the reference engine's
|
||
own psalm-number labels, not assumed. This is the *same* 5-antiphon pool
|
||
Prime/Terce/Sext/None draw from (Prime=group 1, Terce=2, Sext=3, None=5,
|
||
skipping the canticle's own group 4) — real, confirmed structurally (see
|
||
St. Scholastica below), not a coincidence.
|
||
|
||
St. Scholastica needed the same treatment as her P/T/S/N content: no
|
||
English exists in the reference engine's data for her Lauds antiphons,
|
||
capitulum, versicle, or hymn. Four of her five antiphons and her
|
||
capitulum reuse the exact English already translated and verified for
|
||
her Terce/Sext/None content (confirmed same Latin, since Lauds and the
|
||
minor hours share this pool); the canticle antiphon, versicle, and
|
||
6-stanza hymn ("Jam noctis umbræ cóncidunt") are newly translated here.
|
||
**Real gap surfaced by this: the Lauds psalmody-override schema
|
||
(`hours/lauds.ts`'s `BilingualText`) has no per-language status field the
|
||
way `hours/resolve-common.ts`'s `ResolvedText` does** — `psalmParts`
|
||
unconditionally wraps every psalmody antiphon in `verifiedText()`, so her
|
||
self-translated antiphon English can't carry the "draft" marker
|
||
(`.text-draft`) the same content does at Prime/Terce/Sext/None. Flagged
|
||
here, not fixed — would need `BilingualText` (or the override schema
|
||
generally) to carry a status field through to `psalmParts`/`resolvePsalmody`.
|
||
|
||
### Prime/Terce/Sext/None minor-hour overrides (2026-08, new mechanism)
|
||
|
||
Found while investigating why St. Lawrence's octave showed none of his own
|
||
content at Prime/Terce/Sext/None either — these four hours had *no*
|
||
per-feast override mechanism at all before this, unlike Lauds: the psalm
|
||
antiphon always came from the plain weekday default
|
||
(`data/hours/{hour}-antiphons.yml`), and the chapter was always a single
|
||
fixed per-annum/per-weekday text, regardless of what was actually being
|
||
celebrated — even on a saint's own actual feast day, not just during an
|
||
octave.
|
||
|
||
Mechanism: `hours/resolve-common.ts`'s `getMinorHourOverrideId` (same
|
||
duplex-majus+ threshold as Lauds' own override, deliberately kept in sync
|
||
so it's one shared backlog, not per-hour lists that can drift — see
|
||
`resolveOfficeWinner`, which makes this octave-aware the same way Lauds'
|
||
already is), `resolveMinorHourAntiphon`, `resolveMinorHourChapter`. Prime
|
||
additionally needed `isSundayOrFeastOffice` (its capitulum/Preces choice
|
||
between Sunday/feast and ferial forms) made octave-aware — live-verified
|
||
Prime's capitulum stays the Sunday/feast form (1 Tim 1:17) throughout an
|
||
octave, not just the feast's own actual day, so no separate override file
|
||
was needed there, just the existing Sunday-form file reached correctly.
|
||
|
||
**Authored — all 29 duplex-majus+ saints, done (2026-08).** Antiphon +
|
||
chapter for Terce/Sext/None, antiphon only for Prime (its chapter needed
|
||
no new file — see above), for every id in the Lauds backlog list above
|
||
except the 5 `propers: null`-blocked saints (`st-benedict`,
|
||
`all-saints-of-the-benedictine-order`, `st-michael`, `st-maurus-abbot`,
|
||
`ss-placid-and-companions`) — those still need a collect authored first.
|
||
Two saints (`st-scholastica`, `immaculate-conception`) needed corrections
|
||
to the reference engine's own data: Scholastica's minor hours have no
|
||
English rendering at all in the source (Matins-only English coverage for
|
||
that saint) — translated directly, `status.en: draft` throughout;
|
||
Immaculate Conception's antiphons render with diacritics stripped
|
||
compared to the word-for-word-parallel Nativity BVM antiphon — restored
|
||
accentuation to match, `status.la: draft`.
|
||
|
||
### Prime/Terce/Sext/None Commons extraction (2026-08)
|
||
|
||
After the P/T/S/N batch above landed, a systematic diff across all 29
|
||
saints' files (not just the pairs noticed in passing during authoring)
|
||
turned up real shared-Common groups, extracted into `data/propers/common/
|
||
{hour}-antiphon-common-of-*.yml` / `{hour}-capitulum-common-of-*.yml`
|
||
files and wired via a new `SaintRecord.minorHoursCommon` field
|
||
(`calendar/feasts.ts`) — `resolveMinorHourAntiphon`/`resolveMinorHourChapter`
|
||
(`hours/resolve-common.ts`) now fall back to it when a saint has no proper
|
||
file of their own for a given hour. Deliberately a *separate* field from
|
||
the existing (until now unused) `common` field: live-verified the minor
|
||
hours don't always draw from the same Common category as the saint's
|
||
overall classification (see the Confessor-Bishop group below) — reusing
|
||
`common` directly would have gotten that case wrong.
|
||
|
||
Four groups extracted, all confirmed byte-identical via `diff` first, none
|
||
assumed from category alone:
|
||
|
||
- **`common-of-apostles`** (all 4 hours, antiphon + chapter): st-matthias,
|
||
st-barnabas, st-james-the-greater, st-thomas-apostle, ss-simon-and-jude,
|
||
st-luke, st-matthew, st-bartholomew — 8 saints, 56 duplicate files
|
||
collapsed to 7 shared ones.
|
||
- **`common-of-a-confessor-bishop`**: chair-of-st-peter-at-rome and
|
||
chair-of-st-peter-at-antioch share all 4 hours' antiphons *and*
|
||
chapters; st-martin-of-tours shares only the chapters (his own
|
||
antiphons are proper, about his own life). Real finding, not a
|
||
modeling choice: both Chair feasts are `common: common-of-an-apostle`
|
||
in their own saint record (Peter's rank/collect classification), but
|
||
their P/T/S/N content comes from the Common of a Confessor Bishop
|
||
instead — Peter considered *as bishop* of that particular see, for
|
||
that specific commemoration.
|
||
- **`common-of-the-bvm`** (chapter only, Terce/Sext/None): nativity-bvm
|
||
and immaculate-conception share all three; their own antiphons stay
|
||
proper (per-mystery). The Assumption's Sext/None chapters share the
|
||
same *reading body* (Sir 24:15-16, 24:19-20) but close with her own
|
||
proper versicles rather than the Common's plain ones, and her Terce
|
||
chapter is a different reading entirely (Sir 24:11-12, not 24:14) — a
|
||
first-class feast's proper office outranking the plain Common, kept as
|
||
her own fully proper files rather than force-fit into the shared one.
|
||
- **`common-of-st-peter`** (None antiphon, all 3 chapters): st-peter-ad-
|
||
vincula and ss-peter-and-paul. Not a real liturgical Common category —
|
||
both feasts are specifically about St. Peter himself, so his own
|
||
proper texts get reused directly. Extracted anyway since the content
|
||
really is identical, just named honestly rather than dressed up as a
|
||
Common of Saints class.
|
||
|
||
### Paschaltide commons — done (2026-08)
|
||
|
||
The Mark/John-Before-the-Latin-Gate/Philip-and-James "Alleluia" text
|
||
flagged above as an open question is confirmed real, not a coincidence:
|
||
the reference engine's own source has explicit Paschaltide variants —
|
||
`Commune/C1p.txt`/`C2p.txt`/`C3p.txt` ("Commune Apostolorum/Unius
|
||
Martyris Pontificis/Plurimorum Martyrum tempore Paschali") — for exactly
|
||
3 of its 12 Common categories: Apostles, a Martyr-Bishop, Several
|
||
Martyrs. Confessors/Virgins/Doctors/BVM never get one — Alleluia/victory
|
||
imagery is restricted to apostles and martyrs specifically, a real
|
||
liturgical distinction, not an app gap.
|
||
|
||
Mechanism: `resolveMinorHourAntiphon`/`resolveMinorHourChapter`
|
||
(`hours/resolve-common.ts`) now try `${hourId}-antiphon-
|
||
${minorHoursCommon}-paschaltide` (etc.) first whenever `day.season` is
|
||
`eastertide`, `ascensiontide`, or `pentecost` — the same three-season
|
||
window as Compline's own Regina Caeli table
|
||
(`data/hours/marian-antiphon-by-season.yml`), not just `eastertide`
|
||
alone, since "tempore paschali" runs through the Pentecost octave in
|
||
both cases. Falls back to the plain (non-Paschaltide) Common when no
|
||
`-paschaltide` variant is authored, same silent-fallback convention as
|
||
everywhere else.
|
||
|
||
`common-of-apostles-paschaltide` (7 files) ported directly from
|
||
`Commune/C1p.txt`/English `Commune/C1p.txt` — not live-queried — and
|
||
cross-checked against st-mark's already-authored, live-queried content
|
||
(2026-04-25): identical except the source's own Prime-antiphon asterisk
|
||
falls one word earlier than the live-rendered incipit had suggested,
|
||
corrected here to match the source. st-mark and st-john-before-the-
|
||
latin-gate's own duplicate per-saint files removed (all 7 each);
|
||
ss-philip-and-james's chapter files removed (its own antiphons stay
|
||
proper — its own Gospel-referencing text, not shared even in
|
||
Paschaltide). Only `C1p` (Apostles) ported so far, since no currently-
|
||
authored saint sits on a Martyr-Bishop or Several-Martyrs common that
|
||
could fall in Paschaltide — `common-of-a-confessor-bishop` and any
|
||
future Martyr-common saint don't need one (Confessors never get a
|
||
Paschaltide variant at all; a Martyr one would need porting `C2p`/`C3p`
|
||
the same way, if and when a saint needing it is authored).
|
||
|
||
### 94 `propers: null` saints — collects authored (2026-08)
|
||
|
||
**The reference engine's per-date `Sancti/`/`SanctiM/MM-DD.txt` files are
|
||
era-merged, not era-specific — blind extraction from them is unreliable.**
|
||
Live-querying (`version=Monastic Tridentinum 1617`) always resolved this
|
||
correctly elsewhere in this project because the *engine itself* applies
|
||
the right rubric filtering; reading a raw `Sancti/MM-DD.txt` file
|
||
directly bypasses that filtering entirely. Those files accumulate
|
||
centuries of saints assigned to the same calendar date across different
|
||
eras, gated behind `(rubrica ...)`-tagged conditional blocks — reading
|
||
the file's own untagged/default content silently returns whichever saint
|
||
is *currently* primary there (often a post-1617, sometimes post-1888
|
||
addition), not necessarily the one relevant to this app's target era.
|
||
Confirmed the hard way: an initial blind pull returned St. Didacus's
|
||
collect for All Saints of the Benedictine Order, St. Rose of Lima's for
|
||
Ss. Felix and Adauctus, St. Josaphat's for St. Martin I, and 9 more
|
||
wrong matches out of 13 "clean, unambiguous" test cases — all real
|
||
saints, just centuries later than 1617, and none of them what this
|
||
app's own calendar actually intends for that date.
|
||
|
||
**The fix**: `web/www/Tabulae/Kalendaria/` has real, era-specific
|
||
calendar *tables* (not per-date files) — `1570.txt` (Tridentine base)
|
||
and `M1617.txt` (Monastic 1617's own diffs against it) — giving a
|
||
direct, unambiguous answer for "which saint(s), what rank, commemorated
|
||
with whom" on every date this app cares about, plus a `fileref` field
|
||
(e.g. `01-15t`, `08-23o`, sometimes pointing at a wholly different
|
||
MM-DD) identifying *exactly* which source file variant actually holds
|
||
that entry's content — including cross-file (`ex Sancti/05-08`,
|
||
`Commune/C1v:Oratio 2 loco`) and whole-file (`@Sancti/05-08` as the
|
||
file's own first line, no `[Section]` around it) inheritance, all
|
||
resolved by a purpose-built parser (`scratchpad/parse_kalendaria.py` +
|
||
`extract_final.py`, not committed — see the P/T/S/N and Lauds batches'
|
||
own parser scripts for the established one-off-tool convention).
|
||
|
||
Cross-referencing all ~205 saints in `data/calendar/saints/*.yml`
|
||
against this table (not just the 94) found the existing calendar data
|
||
essentially sound — 204/205 confirmed correctly dated (only the
|
||
deliberate `example-confessor` placeholder didn't match, as expected);
|
||
the earlier wrong-saint problem was specific to *this* extraction
|
||
method, not a systemic issue with the app's own calendar. No corrections
|
||
to `sanctoral-calendar.yml` were needed.
|
||
|
||
**Resolved St. Michael's own long-flagged uncertainty** (`st-michael.yml`
|
||
used to carry a "FLAGGED" comment about Sep 29 showing nothing live
|
||
across 6 tested years): the table confirms Sep 29 genuinely is his day
|
||
under Monastic 1617, rank 3 — it just never won outright in any tested
|
||
year. His own source file is a whole-file cross-reference to the
|
||
Apparition of St. Michael's (`ex Sancti/05-08`), confirmed word-for-word
|
||
identical live — `propers` now points directly at
|
||
`apparition-of-st-michael`'s own id rather than duplicating content.
|
||
|
||
**Two collect shapes, both now complete for all 94:**
|
||
- **34 have a genuine proper collect** — Latin pulled directly from
|
||
source (`{id}-collect.yml` in `data/propers/common/`), English
|
||
translated and marked `status.en: draft` for the ~11 where no source
|
||
rendering exists at all (same convention as St. Scholastica's content
|
||
earlier this session), verified/reused where the source does have one.
|
||
- **60 draw from a Common template** — a real architectural addition,
|
||
not a workaround: `SaintRecord.collectCommon`/`collectName`
|
||
(`calendar/feasts.ts`) plus a substitution step in `getDayCollect`
|
||
(`hours/resolve-common.ts`) fill a Common collect's literal `{N}`
|
||
placeholder(s) with the saint's own name(s), positionally — most
|
||
templates need one name, a few (two co-named saints sharing a
|
||
collect, e.g. Ss. Cornelius and Cyprian) need two. 21 distinct
|
||
Common-collect templates now exist (`collect-c*.yml`), named after the
|
||
reference source's own `C`-numbering (`C2`, `C2-1`, `C2a-1`, `C3b`,
|
||
etc.) rather than an invented semantic label, since no clean
|
||
after-the-fact explanation was recoverable for why the tradition
|
||
splits e.g. "Common of a Martyr Bishop" into five differently-worded
|
||
variants. Each saint's own `[Name]` section supplies its declined
|
||
form already in the exact grammatical case that saint's specific
|
||
template needs (confirmed empirically — Ambrose's own `[Name]` is
|
||
accusative, matching his template's own accusative `{N}` slot). One
|
||
real wrinkle: Ss. Popes among the Martyr-Bishop groups (Cletus &
|
||
Marcellinus, Soter & Caius, Anacletus, Anicetus) need the *second*,
|
||
`"(sed communi Summorum Pontificum)"`-tagged declension in their own
|
||
`[Name]` section, not the first — a real, source-documented
|
||
distinction (Popes get their own template variant), not a parsing
|
||
quirk to work around.
|
||
|
||
One genuine judgment call, flagged rather than silently resolved: St.
|
||
Martha's own Monastic-specific source file (`SanctiM/07-29.txt`)
|
||
overrides her base Tridentine assignment (`collect-c6a`, one name) with
|
||
`collect-c6b` (two names) instead — a plural "Several Virgins and
|
||
Martyrs" template that doesn't fit a single saint. Kept the singular
|
||
`collect-c6a` assignment as the more sensible reading (see her own
|
||
file's comment), treating the Monastic override as likely an
|
||
inconsistency in that one file rather than a real joint-commemoration
|
||
convention this app doesn't otherwise model.
|
||
|
||
Verified end-to-end: a runtime smoke test resolving `getDayCollect` for
|
||
all 94 ids via a synthetic sanctoral-winner day, confirming a real
|
||
(non-`missing`, no leftover `{N}`) collect for every one — not just that
|
||
the YAML files parse.
|
||
|
||
### 94 `propers: null` saints — Benedictus antiphon + P/T/S/N Commons (2026-08)
|
||
|
||
The natural next step the collects batch unblocked. Two more real gaps
|
||
found and fixed along the way, both explained below.
|
||
|
||
**Raw `Commune/*.txt` fields don't reliably match what the engine
|
||
actually renders per hour, same lesson as the collects pass but for
|
||
antiphons this time.** `Commune/C2.txt`'s (Martyr-Bishop) own `[Ant 1]`/
|
||
`[Ant 2]`/`[Ant 3]` fields don't correspond 1:1 to Prime/Terce/Sext/None
|
||
the way a naive reading would suggest, and no `CommuneM/*.txt` file
|
||
defines explicit `[Ant Prima]`/`[Ant Tertia]`/`[Ant Sexta]`/`[Ant Nona]`
|
||
fields for any of the categories this batch needed (only `C10`, BVM
|
||
Saturday, has them). Confirmed independently: the already-authored
|
||
`common-of-a-confessor-bishop` P/T/S/N content (from the original
|
||
29-saint pass) doesn't match `Commune/C4.txt`'s own raw `[Ant 1]` either
|
||
— it was live-query-sourced from Chair of St. Peter/St. Martin's own
|
||
rendered pages, not read from the Commune file directly, which is
|
||
exactly why. Fix: queried `web/cgi-bin/horas/officium.pl` directly
|
||
(local Perl, not a network fetch — `prayPrima`/`prayTertia`/`praySexta`/
|
||
`prayNona`/`prayLaudes`, `version=Monastic Tridentinum 1617`) for one
|
||
representative `propers: null` saint per Common category, parsing the
|
||
antiphon (incipit + full, reconstructing the `*` split point) and
|
||
chapter (citation, text, closing versicles) out of the rendered HTML —
|
||
new scratchpad tool `query_hours.py`/`batch_hours.py`, not committed,
|
||
same one-off-tool convention as the rest of this project's extraction
|
||
scripts. Caught its own version of the collects batch's "wrong saint"
|
||
bug this way too: St. Emerentiana's stored clean-year comment
|
||
(2029-01-23) actually rendered St. Hilary of Poitiers's office by that
|
||
year (a stale date after his own earlier Jan 23 → Jan 14 correction,
|
||
see above) — every representative's title is now verified against an
|
||
accent-insensitive name-stem match before its content is trusted, with a
|
||
fallback year search when the stored date doesn't check out.
|
||
|
||
**The real liturgical categories are coarser than `SaintRecord.common`'s
|
||
naming suggests — verified by cross-querying multiple saints per
|
||
category, not assumed.** `common-of-a-martyr-bishop` and
|
||
`common-of-a-martyr` render byte-identical P/T/S/N and Benedictus
|
||
antiphon content (confirmed: St. Anacletus, St. Simeon, St. Blaise, St.
|
||
Pantaleon — four different collect-template variants, one shared minor-
|
||
hour Common); so do `common-of-an-abbot`, `common-of-a-confessor-not-
|
||
bishop`, and `common-of-a-confessor` (St. Odo, St. John Gualbert, St.
|
||
Nicholas of Tolentino, St. Alexius); so do `common-of-a-virgin-martyr`,
|
||
`common-of-a-virgin`, and `common-of-a-holy-woman` (St. Justina, St.
|
||
Emerentiana, St. Praxedes, St. Martha). `common-of-several-martyrs`
|
||
splits further than its own name suggests, though: plain groups of
|
||
martyrs (Ss. Cornelius and Cyprian, Ss. Faustinus and Jovita, Ss. Placid
|
||
and Companions — the last a real Duplex II. classis feast, not just a
|
||
low-rank stub, confirming the fallback isn't only reached by minor
|
||
saints) share one Common, but groups of *Pope*-martyrs (Ss. Cletus and
|
||
Marcellinus, Ss. Soter and Caius) render distinct, more triumphal,
|
||
allelúja-toned text of their own — a real category the collect
|
||
templates already knew about (`collect-c3b`) that the `common` field
|
||
alone doesn't surface. One saint, the Dedication of the Basilicas of Ss.
|
||
Peter and Paul, turned out *not* to share its `common` field's nominal
|
||
category (`common-of-an-apostle`) at all: live-verified its P/T/S/N and
|
||
Benedictus antiphon are the Dedication-of-a-Church Common instead (about
|
||
the building, matching its own `collect-c8`), not shared with any
|
||
apostle content. Net result: 7 new shared Common categories
|
||
(`common-of-a-martyr`, `common-of-an-abbot`, `common-of-several-
|
||
martyrs`, `common-of-pope-martyrs`, `common-of-a-vigil`, `common-of-a-
|
||
virgin`, `common-of-a-dedication`) plus the pre-existing `common-of-a-
|
||
confessor-bishop` cover 66 of the 94 saints; the 7th confessor-bishop
|
||
saint's own Benedictus antiphon Common file was also new (the original
|
||
29-saint pass never needed it — Chair of St. Peter/St. Martin both have
|
||
their own proper Lauds antiphon, only their chapters were shared).
|
||
|
||
**New mechanism**: `SaintRecord.benedictusCommon` (`calendar/feasts.ts`)
|
||
— `getBenedictusAntiphon` (`hours/resolve-common.ts`) now falls back to
|
||
`benedictus-antiphon-${benedictusCommon}.yml` when a saint has neither a
|
||
proper collect-antiphon pair nor... (see below) — deliberately no name-
|
||
substitution mechanism built for it (unlike `collectCommon`): every
|
||
Common category in current use renders name-free Benedictus-antiphon
|
||
text; only the Doctor Common (`C4a`, not used by any saint here) has a
|
||
`{N}`-shaped placeholder in the source.
|
||
|
||
**Real bug found and fixed while wiring this in**: `getMinorHourOverrideId`
|
||
gated *all* Prime/Terce/Sext/None lookup — both the per-saint proper file
|
||
and the shared-Common fallback — 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 at all. Live-querying
|
||
this batch proved that wrong: a winning Semiduplex saint (St. Anacletus)
|
||
still renders its own proper/Common antiphons at Prime/Terce/Sext/None,
|
||
not the plain ferial default — the majus+ gate was only ever a proxy for
|
||
"this app has authored proper content," not a real liturgical rule, and
|
||
became actively wrong once Common-category content started covering
|
||
lower ranks too. Fixed by making `getMinorHourOverrideId` eligible for
|
||
*any* sanctoral winner; Lauds' own psalmody override
|
||
(`hours/lauds.ts`'s `getPsalmodyOverrideFor`) is unaffected and correctly
|
||
stays majus+-gated, since that's a real, much bigger per-saint authored
|
||
commitment (the whole psalm-antiphon block), not something this batch
|
||
adds a Common fallback for.
|
||
|
||
One saint, Ss. Ursula and Companions, deliberately left without
|
||
`minorHoursCommon`/`benedictusCommon`: her collect template
|
||
(`collect-c6b`, plural Virgins and Martyrs) has no other member on this
|
||
app's calendar to verify a shared minor-hour text against, and she can
|
||
never be queried directly herself (a genuinely unwinnable stub, always
|
||
commemorated under St. Hilarion) — left honestly unauthored rather than
|
||
guessed from the singular `common-of-a-virgin` category, moot in
|
||
practice since a commemoration's own minor hours are never rendered.
|
||
|
||
Verified end-to-end: a runtime smoke test (folded into
|
||
`tests/hours/resolve-common.test.ts`) resolving `getBenedictusAntiphon`/
|
||
`resolveMinorHourAntiphon`/`resolveMinorHourChapter` for a sample across
|
||
categories, including the Duplex II. classis case to confirm the
|
||
majus+-gate fix isn't just a low-rank special case.
|
||
|
||
**Not done in this pass**: the other 26 of the 94 (`propers` now set to
|
||
their own id, from the collects batch's proper-collect saints) still
|
||
need their own individual Benedictus antiphon authored — same shape as
|
||
the original 109-saint pass (own `[Ant 1]`/live-rendered text, not a
|
||
shared Common), since a saint with a genuine proper collect usually has
|
||
a genuine proper antiphon too. Not yet started.
|
||
|
||
### The other 26 propers-set saints' Benedictus antiphons — done (2026-08)
|
||
|
||
Turned out **most of these don't have unique antiphon text either** —
|
||
a genuine proper collect doesn't imply a genuine proper antiphon, same
|
||
finding as St. Martin of Tours's mixed proper-antiphon/shared-chapter
|
||
split from the original 29-saint pass. Live-queried Lauds
|
||
(`prayLaudes`) for each of the 26, same title-verified method as the
|
||
Common-category batch above.
|
||
|
||
**15 of the 26 are genuinely unwinnable stubs** (already documented as
|
||
such from the collects pass — always commemorated, never the day's own
|
||
title, so nothing to query) — same "honestly missing" treatment as St.
|
||
Frances of Rome/St. Bibiana in the original 109-saint pass, not
|
||
guessed at.
|
||
|
||
**Of the 11 that do win outright**, 7 turned out to share existing
|
||
Common-category text — `benedictusCommon` wired directly to the already-
|
||
authored files from the section above, no new content needed: Ss.
|
||
Dionysius and Companions, Ss. Felix and Adauctus, Ss. Maurice and
|
||
Companions, Ss. Tryphon and Companions (`common-of-several-martyrs`,
|
||
despite Dionysius's own `common` field saying `common-of-a-martyr-
|
||
bishop` — another case, like Chair of St. Peter earlier, where the two
|
||
fields genuinely diverge), St. Chrysogonus and St. Donatus
|
||
(`common-of-a-martyr`), the Vigil of St. Lawrence (`common-of-a-vigil`).
|
||
|
||
The remaining 4 have real, unique proper text — new
|
||
`{id}-antiphon.yml` files: All Saints of the Benedictine Order ("Vos
|
||
qui reliquístis ómnia..."), St. Agnes (secunda) ("Ecce, quod
|
||
concupívi..."), St. Benedict ("Sanctíssime Conféssor Dómini..." — no
|
||
English in the source, same gap as St. Scholastica's, translated and
|
||
marked `draft`), and the Vigil of St. John the Baptist ("Eréxit nobis
|
||
* Dóminus cornu salútis..." — deliberately echoing the Benedictus
|
||
canticle's own opening words, fitting for the Vigil of the saint whose
|
||
father, Zechariah, first spoke that canticle).
|
||
|
||
P/T/S/N for these 26 not attempted in this pass — 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 (duplex-majus+ subset only). Done
|
||
in a later pass, see "`minorHoursCommon` gap swept across the whole
|
||
calendar" below — it covers these 26 too, not just the original 109.
|
||
|
||
### The 15 "unwinnable" stubs' Benedictus antiphons, resolved from source (2026-08)
|
||
|
||
Direct feedback on the section above: "unwinnable" was describing an
|
||
artifact of Divinum Officium's own single-winner precedence model, not
|
||
a real content gap — the app doesn't need a saint to *win* a live query
|
||
to have real content, since this app already renders commemorated
|
||
saints on their own terms (`day.commemorations`) rather than only ever
|
||
showing the day's outright winner. The fix: read each of the 15 saints'
|
||
own already-validated fileref directly (`extract_final.py`'s
|
||
`load_file`/`resolve`, the exact same fileref their collect was already
|
||
sourced from during the Kalendaria-table pull — no live query, no
|
||
"does this saint ever win" requirement at all) 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 `[Rule]`/`[Rank]` C-numbers (`C2a`,
|
||
`C2a-1`, `C3`, `C3a`, `C3a-1`, `C4b`, `C6`, `C6-1`, `C6a`, `C7`) falls
|
||
inside a family already live-query-verified in the section above, so
|
||
`minorHoursCommon`/`benedictusCommon` were wired directly with full
|
||
confidence, no new content needed: St. Marcus (`C4b` — a whole-file
|
||
`@Commune/C4` inherit, just the title relabeled "of the Popes" — into
|
||
`common-of-a-confessor-bishop`); St. Felix (Presbyter), St. Hermes, St.
|
||
Saturninus (`C2a`/`C2a-1` into `common-of-a-martyr`); St. Prisca, St.
|
||
Thecla, St. Catherine of Siena (`C6`/`C6-1`/`C6a` into
|
||
`common-of-a-virgin`); Ss. Machabees, Ss. Marius/Martha/Audifax/Abachum,
|
||
Ss. Twelve Brothers, Ss. Alexander/Eventius/Theodulus/Juvenal, Ss.
|
||
Felix/Simplicius/Faustinus/Beatrix, Ss. Nabor and Felix
|
||
(`C3`/`C3a`/`C3a-1` into `common-of-several-martyrs`). St. Felicitas is
|
||
the one genuine new finding: her file points to `C7` ("Commune Unius
|
||
non Virginis Martyris"), which isn't a category this project had
|
||
touched yet — but `C7`'s own `[Ant 1]` is itself a cross-reference
|
||
(`@Commune/C6:Ant 2`) that resolves to text byte-identical to
|
||
`common-of-a-virgin`'s own already-verified Benedictus antiphon, so no
|
||
new Common file was needed even there.
|
||
|
||
**The 1 remaining (Ss. Euphemia, Lucy, and Geminian) does have her own
|
||
real `[Ant 1]`** — but marked `status.la: draft`, not `verified`, on
|
||
principle: a real counterexample turned up in the *same* pass while
|
||
double-checking a winnable saint for comparison — St. Alexius's own
|
||
file also has a genuine `[Ant 1]` ("Similábo eum..."), yet his actual
|
||
live-rendered Benedictus antiphon (already confirmed in the section
|
||
above) uses `common-of-an-abbot`'s text instead, not his own `[Ant 1]`
|
||
at all. So a raw `[Ant 1]` read is demonstrably not sufficient proof by
|
||
itself that it's what the Benedictus canticle actually uses (Vespers'
|
||
Magnificat is a live candidate for what it's *actually* for), even for
|
||
a saint that *can* be live-checked — and for one that never can be, that
|
||
uncertainty can't be resolved at all. Used the text anyway (better than
|
||
nothing, and plausibly correct), just honestly flagged rather than
|
||
claimed as confirmed.
|
||
|
||
All 15 now have real content — none left "honestly missing." Ss.
|
||
Ursula and Companions (the section above) is the one case that's
|
||
actually different in kind: no other calendar member shares her
|
||
`C6b`/plural-Virgins-and-Martyrs category to cross-check a Common
|
||
against, so even a clean file read wouldn't have anything solid to
|
||
verify itself against.
|
||
|
||
### A full sweep for the same pattern elsewhere, and 2 more real fixes (2026-08)
|
||
|
||
Same feedback, applied more broadly: swept every saint in
|
||
`data/calendar/saints/*.yml` (not just the 94-batch's own lineage) for
|
||
a still-`missing` collect or Benedictus antiphon, via a synthetic-winner
|
||
smoke test. Found 4 hits; 2 were real, 2 were false positives from the
|
||
sweep's own synthetic-day methodology (forcing a saint to be `day.winner`
|
||
when in reality it's *never* the winner doesn't correctly exercise its
|
||
real content path):
|
||
|
||
- **St. Frances of Rome and St. Bibiana** (Benedictus antiphon) —
|
||
exactly the fix from the section above, just predating the
|
||
Kalendaria-table work (no fileref had been derived for either yet).
|
||
Both resolve to `common-of-a-virgin` once one is: Bibiana's file
|
||
points to `C6-1` directly; Frances of Rome's points to `C7a` (a
|
||
whole-file inherit of `C7`, just the `[Officium]` title relabeled "of
|
||
a Widow" — fitting, since she's a widow, not a virgin — with no
|
||
override of the same `[Ant 1]` cross-reference, so it resolves
|
||
identically).
|
||
- **St. Agatha and St. Lucy** (collect) — a real, older gap, from
|
||
*before* `collectCommon` existed at all: both saints' own file
|
||
comments already documented "no unique collect... resolves honestly
|
||
missing... despite propers being set," written back when that was the
|
||
only option. Agatha's own `[Oratio]` is `@Commune/C6::s/N\./Agathæ/` —
|
||
confirmed identical to the already-existing `collect-c6.yml` template
|
||
— so `collectCommon: collect-c6` was simply wired in. Lucy's own
|
||
`[Oratio]` is `@Commune/C7a`, *unedited* (unlike `collect-c6a.yml`'s
|
||
own C7a-derived text, which has a `"Vírginis tuæ"` insertion baked
|
||
in) — her own `[Name]` section supplies the whole descriptive phrase
|
||
instead ("Lúciæ Vírginis et Mártyris tuæ", not just her bare name),
|
||
making her resolved text effectively proper even though the
|
||
underlying formula is a shared Common one; written as her own
|
||
`st-lucy-collect.yml` (she already had `propers: "st-lucy"` set)
|
||
rather than a new single-use `collect-c7a` template. No English
|
||
`[Oratio]` in the source for either saint's own unedited form, so
|
||
Lucy's stays `status.en: draft`.
|
||
- **St. Clare** (false positive) — the sweep's synthetic day forced her
|
||
to be `day.winner`, but she never actually is one (always
|
||
commemorated under St. Lawrence's octave); her real content lives in
|
||
`st-clare-commemoration.yml`, used via a completely different code
|
||
path (`sanctoralCommemorationPart`, not `getBenedictusAntiphon`) that
|
||
the sweep doesn't exercise. Already fully authored and live-verified;
|
||
no gap.
|
||
- **Ss. Ursula and Companions** (already covered above) — the one
|
||
genuine, currently-irresolvable case.
|
||
|
||
Two vigils (Vigil of St. John the Baptist, Vigil of St. Lawrence) didn't
|
||
resolve via this file-based method at all (their particular file format
|
||
wasn't handled by this pass's parser) — no impact, both already had
|
||
real content from the live-query batch above.
|
||
|
||
### `minorHoursCommon` gap swept across the whole calendar (2026-08)
|
||
|
||
The pieces above all fixed Benedictus-antiphon/collect gaps. This pass
|
||
does the same thing for Prime/Terce/Sext/None specifically, and widens
|
||
scope from just the 94-saint batch to the **whole** calendar — the
|
||
original 29-saint P/T/S/N pass (an earlier session) only ever covered
|
||
duplex-majus+ saints; every below-majus saint from the *original*,
|
||
pre-94-batch 109-saint pass had never had P/T/S/N attempted at all,
|
||
same gap as the one already found and fixed for the 94-batch's own
|
||
below-majus saints.
|
||
|
||
**Method**: reused the Kalendaria-table-validated fileref approach
|
||
(same as the collects pass), not 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. The Kalendaria table's own
|
||
validated fileref (`02-09o`) gives her real content instead. Also fixed
|
||
a second, unrelated regex bug the same pass turned up: the original
|
||
"`vide`/`ex CXX`" pattern matched the bare word "`Commune`" (no number)
|
||
and reading-specific sub-references like "`ex C4 in 2 loco`" (which
|
||
only says where *that nocturn's readings* come from, not the overall
|
||
antiphon Common) — found via St. Clement, whose Monastic-specific
|
||
`[Rule]` override has neither a clean `vide CXX` nor a number after
|
||
"Commune." Fixed by requiring a digit right after `C` and skipping any
|
||
match immediately followed by `"in N loco"`.
|
||
|
||
**90 below-majus saints swept, ~82 wired directly** into the categories
|
||
already established above (mostly `common-of-a-martyr` and
|
||
`common-of-several-martyrs`, the two largest families) — no new content
|
||
needed, since every C-number variant found (`C2`/`C2-1`/`C2a`/`C2a-1`/
|
||
`C2b`/`C2b-1`/`C2-1b`, `C3`/`C3a`/`C3a-1`, `C5`/`C5-1`/`C5a`/`C5b`,
|
||
`C6`/`C6-1`/`C6a`/`C7a`, `C4`/`C4b`, `C1`, `C1v`) already falls inside a
|
||
family live-query-verified earlier.
|
||
|
||
**A real bug this pass caught and fixed**: Ss. Ursula and Companions
|
||
(deliberately left `minorHoursCommon`-unwired, see her own section
|
||
above) got swept up and *wrongly* wired to `common-of-an-abbot` by this
|
||
pass's own script, since she has `collectCommon` set (making her look
|
||
like a normal, resolvable batch member) despite `propers: null` and
|
||
being a genuine commemoration-only stub — the underlying
|
||
Kalendaria-table lookup silently defaulted to St. Hilarion's own
|
||
`primary` slot at her shared date rather than her own (nonexistent)
|
||
one. Caught by cross-checking every swept saint's `propers` field for
|
||
this exact `null`-but-`collectCommon`-set pattern before trusting a
|
||
default-to-primary assumption; reverted, she's still correctly unwired.
|
||
|
||
**A real, new category found: Doctors of the Church are not one thing.**
|
||
7 saints' own `[Rule]` says "`vide C4a`" (`Commune Doctoris Pontificis`)
|
||
— but live-querying showed this doesn't mean what it looks like it
|
||
means. St. Athanasius, St. Augustine, St. Basil the Great, St. Hilary of
|
||
Poitiers, St. John Chrysostom, and St. Leo I all render P/T/S/N
|
||
byte-identical to `common-of-a-confessor-bishop`'s already-verified
|
||
text (their own `C4a` reference just inherits `C4`'s content
|
||
underneath, per the same "the referenced file's raw fields don't
|
||
reliably predict the live-rendered text" lesson from every other
|
||
category this session) — wired directly, no new content. **St. Gregory
|
||
the Great is the one genuine exception**: his own live-rendered
|
||
P/T/S/N is real, unique proper text (alluding to his own election,
|
||
his *Liber Regulæ Pastoralis*, the dove-and-quill legend, and the
|
||
"non Angli sed Angeli" episode already known from his Benedictus
|
||
antiphon) — 7 new files (`{hour}-antiphon-st-gregory-the-great.yml`,
|
||
`{terce,sext,none}-capitulum-st-gregory-the-great.yml`), no
|
||
`minorHoursCommon` needed at all (`getMinorHourOverrideId` already
|
||
tries his own id first). English wasn't rendered at all for any of his
|
||
6 new texts (same Latin-duplicated-into-English gap as St. Scholastica/
|
||
St. Benedict), so all 6 are `status.en: draft`.
|
||
|
||
**A loose thread noticed, then chased down and fixed (2026-08)**: St.
|
||
Athanasius (May 2) and St. Leo I (April 11), both tested in years that
|
||
happened to fall within Eastertide, rendered their shared
|
||
`common-of-a-confessor-bishop` text with a trailing "`allelúja`"
|
||
appended to every antiphon — but the other 4 Doctor saints (tested in
|
||
non-Paschaltide months) didn't. Live-querying further (a full sweep of
|
||
Common categories across the Easter/Ascension/Pentecost octaves and the
|
||
ordinary weeks around them, reading the reference source's raw
|
||
`Commune/*p.txt` chains directly rather than guessing from rendered
|
||
pages) settled it precisely:
|
||
|
||
- A handful of categories (Apostles, Martyr, Martyr-Bishop, Several
|
||
Martyrs, Pope-Martyrs) have their own dedicated `Cxxp.txt` source
|
||
chain — a genuinely *different* text for Paschaltide, not the same
|
||
text plus a suffix (confirmed by reading the raw files: e.g. the
|
||
"Fíliæ Jerúsalem..." Pope-Martyrs Benedictus antiphon comes from a
|
||
Matins responsory reused for Lauds specifically during this window,
|
||
nowhere near the non-Paschaltide chain's own "Mártyrum chorus..."
|
||
text). Only `common-of-apostles` has this wholesale text actually
|
||
authored into this app's data (`*-paschaltide.yml`, from an earlier
|
||
pass) — the other four still fall through to the mechanical suffix
|
||
below until someone authors their real text, which is a strictly
|
||
better approximation than the unmodified default.
|
||
- Every *other* category (Confessor-Bishop/Doctor, Abbot, Virgin, and
|
||
presumably Dedication/Vigil/Angel, untested but structurally the
|
||
same) has no dedicated chain at all — the reference engine just
|
||
mechanically appends a single ", allelúja"/", alleluia" to the
|
||
otherwise-unchanged base text, for the whole Eastertide → Ascensiontide
|
||
→ Pentecost-octave-week span. Live-verified across three unrelated
|
||
categories (St. Athanasius/Confessor-Bishop, St. Robert/Abbot, St.
|
||
Catherine of Siena/Virgin) — never doubled.
|
||
- The "sometimes 1, sometimes 2" appearance wasn't actually the same
|
||
category flipping — it was different *ranks of saint* getting
|
||
different octave privileges. Only martyr-class saints (already in the
|
||
dedicated-chain group above) are ever eligible for even a
|
||
commemoration inside a true privileged octave (confirmed: St.
|
||
Anicetus, commemorated on a Semiduplex day inside the actual Easter
|
||
octave, correctly showing the doubled dedicated-chain text).
|
||
Confessor/Abbot/Virgin-class saints never get that far — checked St.
|
||
Leo I and St. Basil the Great both landing on/near a real octave in
|
||
some year, and both are fully transferred, no commemoration at all —
|
||
so the plain-suffix categories structurally can never be observed
|
||
inside a real octave, only in the ordinary weeks around it.
|
||
- One practical check this settled without needing a code change:
|
||
`common-of-pope-martyrs` (Ss. Soter & Caius, Ss. Cletus & Marcellinus)
|
||
is itself one of the dedicated-chain categories, so its one stored
|
||
file is technically the Paschaltide-only text, not a universal
|
||
default — but tracing both saints' fixed dates against every possible
|
||
Easter date confirms this is harmless: whenever their date would fall
|
||
outside Eastertide, it lands inside the Sacred Triduum instead
|
||
(always fully transferred, never rendered), so the stored text is
|
||
correct for every real occurrence.
|
||
|
||
Built: `hours/resolve-common.ts`'s `appendPaschaltideAlleluia`/
|
||
`withPaschaltideAlleluia`, applied to the Common-category fallback
|
||
branch of `getBenedictusAntiphon` and `resolveMinorHourAntiphon` (not
|
||
the `-paschaltide`-file branch, which already had its own correct
|
||
wholesale text; not a saint's own unique `${propers}-antiphon`, which
|
||
would need its own live-requery to confirm one way or the other rather
|
||
than assuming the Common-category finding transfers; not capitula,
|
||
whose closing versicles get the same suffix in the reference engine too
|
||
— confirmed, St. Robert's Terce capitulum — but whose text is one
|
||
embedded R./V. blob per language rather than a clean appendable tail,
|
||
not attempted this pass). Guards against double-appending on a category
|
||
whose stored text already ends in "allelúja" (`common-of-pope-martyrs`,
|
||
per above). 4 new tests in `tests/hours/resolve-common.test.ts`
|
||
(`Paschaltide alleluia suffix on Common-category antiphons`), 300 tests
|
||
passing.
|
||
|
||
Verified end-to-end: a runtime smoke test (folded into
|
||
`tests/hours/resolve-common.test.ts`) covering the same-person reuse
|
||
case (Finding of St. Stephen → St. Stephen Protomartyr's own files),
|
||
Gregory's unique content, and a shared-Doctor case, plus 8 more spot
|
||
checks across the newly-wired families during the pass itself.
|
||
|
||
### The last 2 duplex-majus+ P/T/S/N gaps, and a final-sweep bug (2026-08)
|
||
|
||
Closes out `minorHoursCommon`/P/T/S/N entirely: the original 29-saint
|
||
pass held back 5 saints for lacking a collect at the time (`st-benedict`,
|
||
`all-saints-of-the-benedictine-order`, `st-michael`, `st-maurus-abbot`,
|
||
`ss-placid-and-companions`) — the latter two already got Common
|
||
fallbacks wired in earlier sections above, and St. Michael redirects to
|
||
Apparition of St. Michael's own content (see below). That left **St.
|
||
Benedict and All Saints of the Benedictine Order** as the only two
|
||
duplex-majus+ saints on the whole calendar with no P/T/S/N at all.
|
||
Live-queried both the same way the original 29 were: real, unique
|
||
proper text for each (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 total, English translated
|
||
throughout (`status.en: draft`) since the source rendered none for
|
||
either saint's own texts.
|
||
|
||
**A final calendar-wide sweep** (every saint's `resolveMinorHourAntiphon`/
|
||
`resolveMinorHourChapter`, synthetic-winner methodology) turned up one
|
||
more real gap: **St. Michael himself**. His `propers` field points at
|
||
Apparition of St. Michael's own id (the two feasts are word-for-word
|
||
identical, already established), and `getDayCollect`/
|
||
`getBenedictusAntiphon` both explicitly follow `propers` when looking
|
||
up their own content — but `getMinorHourOverrideId` doesn't; it keys
|
||
off the day's own winner id directly (`"st-michael"`, not
|
||
`"apparition-of-st-michael"`), so `resolveMinorHourAntiphon` was looking
|
||
for a `prime-antiphon-st-michael.yml` that was never going to exist.
|
||
Fixed the same way as Finding of St. Stephen: `minorHoursCommon:
|
||
apparition-of-st-michael` on his own record, reusing his delegate's
|
||
already-authored proper files directly. A real, narrow gap in the
|
||
mechanism itself (not just missing data) — worth remembering if any
|
||
other saint's `propers` ever points somewhere other than its own id.
|
||
|
||
**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
|
||
(deliberately left unwired, no way to verify a category of one) and
|
||
the `example-confessor` test placeholder, which was never meant to
|
||
have any.
|
||
|
||
### Christmas-Octave Sunday content — done (2026-08)
|
||
|
||
Was: only the Benedictus antiphon existed for `christmas-octave-sunday`
|
||
(`christmas-octave-sunday-benedictus-antiphon.yml`) — no collect (a
|
||
separate, older file, `christmas-octave-sunday-collect.yml`, did already
|
||
exist and was untouched here), no Lauds psalmody override or office
|
||
bundle, no Prime/Terce/Sext/None content at all.
|
||
|
||
Real code gap found and fixed along the way: `getMinorHourOverrideId`
|
||
(`hours/resolve-common.ts`) only ever checked the *sanctoral* branch of
|
||
`resolveOfficeWinner`'s result — a named temporal feast in
|
||
`ALWAYS_OVERRIDE_TEMPORAL_IDS` (`marian-saturday`, `christ-the-king`) got
|
||
a Lauds psalmody override via `getPsalmodyOverrideFor`'s own separate
|
||
check of that set, but silently *never* a Prime/Terce/Sext/None one,
|
||
however much content got authored for it — nothing had actually
|
||
exercised that path until now. Fixed by mirroring `getPsalmodyOverrideFor`
|
||
`hours/lauds.ts`'s own temporal-id branch.
|
||
|
||
Content itself needed a different source than every other pull this
|
||
session: Monastic Tridentinum 1617 has no real "Dominica Infra Octavam
|
||
Nativitatis" identity of its own under its own calendar rules on most
|
||
Dec 26-29 Sundays (verified live: 2026-12-27 renders as "S. Joannis
|
||
Apostoli," not the Sunday, under `version=Monastic Tridentinum 1617` —
|
||
only the collect happens to be old enough to have been pulled from a
|
||
date, 2029-12-30, where 1617 *did* render it outright). Lauds psalmody,
|
||
office bundle, and Prime/Terce/Sext/None content instead pulled live
|
||
under `version=Divino Afflatu 1954` (2026-12-27), consistent with this
|
||
app's own stated precedent of following D.A. for the calendar/precedence
|
||
layer even while keeping Monastic 1617's shape for the hours themselves —
|
||
this is one of the rare days where that split genuinely can't be
|
||
maintained, since 1617 has nothing of its own to shape. `christmas-
|
||
octave-sunday-collect.yml`'s existing 1617-sourced text was kept as-is
|
||
(cross-checked identical against both the new D.A. query and the raw
|
||
Monastic Tridentinum 1617 source file, `Tempora/Nat1-0.txt`'s own
|
||
`[Oratio]`) rather than replaced.
|
||
|
||
Files: `lauds-psalmody-overrides/christmas-octave-sunday.yml` (3 psalms +
|
||
Canticle of the Three Young Men + Laudate, all distinct antiphons),
|
||
`lauds-capitulum-/-versicle-/-hymn-christmas-octave-sunday.yml` ("A solis
|
||
ortus cardine"). No `lauds-responsory-christmas-octave-sunday.yml`
|
||
authored — genuinely absent from the live rendering (hymn leads straight
|
||
into the versicle, no short responsory in between), not an oversight;
|
||
left "missing" honestly rather than invented. Prime/Terce/Sext/None all
|
||
four get a proper antiphon (a short "Allelúia, ..." set drawn from the
|
||
day psalter, distinct per hour); Terce/Sext/None each get a proper
|
||
chapter (Gal 4:1-2/4:4-5/4:7, matching the raw 1617 source's own
|
||
`Nat1-0.txt` exactly) with its own versicle per the D.A. live rendering
|
||
(not the 1617 source's own versicle, which reuses one versicle across
|
||
all three hours — a cross-era mismatch flagged here, not silently
|
||
papered over, since this whole day's content already mixes eras out of
|
||
necessity).
|
||
|
||
Not attempted: `Nat2-0` (Feast of the Holy Name of Jesus) isn't in this
|
||
app's sanctoral/temporal calendar at all yet — a separate, bigger ask if
|
||
wanted (see the original scoping discussion this session).
|
||
|
||
### Overlapping octaves — done (2026-08)
|
||
|
||
Was: `resolveOfficeWinner`/`getDayLabel` picked whichever active octave
|
||
started *first* whenever more than one was active — an accident of
|
||
insertion order, not a real precedence rule. St. Lawrence's octave (Aug
|
||
10-17) and the Assumption's (Aug 15-22) genuinely overlap on Aug 16-17
|
||
every year, so this wasn't hypothetical.
|
||
|
||
Design (per direct instruction, not independently re-derived from a
|
||
primary source the way most of this project is): when more than one
|
||
octave is active on a day with no temporal standing of its own to yield
|
||
to first, compare them by rank — highest wins outright, exactly like a
|
||
single octave already out-ranks a weak rival saint; every other active
|
||
octave still gets commemorated, never silently dropped. A rank tie goes
|
||
to whichever octave started more *recently* — reasoning given: day 1 of
|
||
a newly started octave needs to be fully present, which is the whole
|
||
point of an octave starting, so the older one yields instead. No real
|
||
tied-rank case exists yet in this app's calendar to verify that
|
||
empirically, unlike almost everything else in this project — flagged as
|
||
a stated, unverified-by-evidence design decision, not a live finding.
|
||
|
||
Also needed `OctaveConfig.closingDayRank` (default `duplex`): an
|
||
octave's own *closing* day ("in Octava," the 8th/final day) is elevated
|
||
above its ordinary in-between-days rank — live-verified as a real,
|
||
general pattern (not a St. Lawrence quirk): both his own Aug 17 and the
|
||
Assumption's own Aug 22 show as Duplex, above their otherwise-Semiduplex
|
||
ordinary days. This is *why* Lawrence's octave beats the Assumption's on
|
||
their one real overlap day (Aug 17) despite the Assumption being the far
|
||
higher-ranked feast overall (Duplex I. classis vs. Lawrence's Duplex II.
|
||
classis) — his closing-day elevation (Duplex) beats her ordinary day-3
|
||
strength (Semiduplex). Feeds both `strictestThreshold` (a rival saint
|
||
now needs Duplex+, not just Semiduplex+, to beat an octave specifically
|
||
on its own closing day) and the new octave-vs-octave comparison
|
||
(`calendar/octaves.ts`'s `resolveActiveOctave`/`pickWinningOctave`) —
|
||
same underlying "how strong is this octave today" question, two
|
||
different comparison partners.
|
||
|
||
`getDayLabel` was carrying its own separate, never-actually-correct
|
||
octave-selection logic that additionally never checked
|
||
`temporalCategory` at all — found while testing the real Aug 16 overlap
|
||
(a Sunday that year): it was showing an octave name even though the
|
||
*temporal* Sunday has real standing and wins outright, live-verified
|
||
counterexample being the Christmas Octave's own stack (Dec 30), which
|
||
was wrongly showing "3rd Day within the Octave of The Holy Innocents"
|
||
instead of the correct plain temporal label. Fixed by having
|
||
`getDayLabel` share the exact same `ordinary-feria` gate and
|
||
`resolveActiveOctave` call as `resolveOfficeWinner`, so the two can never
|
||
disagree about which identity governs a day.
|
||
|
||
### Dec 26-30's own Sunday rule — done (2026-08), a deliberate remix
|
||
|
||
Per direct instruction — a real, admitted departure from Monastic 1617
|
||
itself, not something independently verified live the way almost
|
||
everything else here is: within Dec 26-30 inclusive (every year contains
|
||
exactly one real Sunday somewhere in that 5-day window), whichever date
|
||
is the actual Sunday always celebrates Nat1-0 (the Sunday within the
|
||
Octave of the Nativity) outright, with **no rank check at all** — even a
|
||
Duplex-II-classis saint (Stephen/John/Innocents) is fully displaced, not
|
||
just commemorated. The displaced saint isn't commemorated in place
|
||
either; he reappears in full as the day's own winner on Dec 30 instead —
|
||
the block's one date with no fixed saint of its own. On a year Dec 30
|
||
itself is the actual Sunday, nothing is displaced and it resolves clean.
|
||
|
||
This intentionally diverges from what Monastic 1617 itself does on Dec
|
||
26-29 (there, live-verified, the Duplex-II-classis saint wins and the
|
||
Sunday is merely commemorated — see the December-sanctoral tests) and
|
||
from the general `privileged-sunday` transfer rule elsewhere in this app
|
||
(which only transfers ranks below duplex-majus and commemorates
|
||
everything at or above it in place) — this window transfers *every*
|
||
rank, full stop, and never commemorates the loser in place.
|
||
|
||
Implemented as its own dedicated, narrowly-scoped layer
|
||
(`calendar/index.ts`'s `applyChristmasOctaveSunday`, same architectural
|
||
pattern as `applyMarianSaturday`/`applyChristTheKing`) rather than by
|
||
changing the shared `decideOccurrence`/transfer machinery other Sundays
|
||
rely on — deliberately contained blast radius. Dec 31 and everything
|
||
outside Dec 26-30 is untouched; christmastide's Sunday there still uses
|
||
the plain `ordinary-sunday` category as before (St. Silvester still
|
||
beats the Sunday on Dec 31, live-verified, unaffected by this).
|
||
|
||
### Benedictus antiphons for temporal (non-sanctoral) days — done
|
||
|
||
`<id>-benedictus-antiphon.yml` in `data/propers/temporal/` — all 54
|
||
temporal ids now have one (`marian-saturday`/`christ-the-king` from
|
||
earlier, the 51 numbered Sundays/seasons done in one pass, plus
|
||
`christmas-octave-sunday` itself, corrected below).
|
||
|
||
**`christmas-octave-sunday` correction**: originally left unauthored
|
||
here, on the theory that every date this id can resolve to collides with
|
||
a specific octave-day saint. Wrong — the reference engine has its own
|
||
dedicated proper for this Sunday specifically, `Tempora/Nat1-0`
|
||
(`web/www/horas/Latin/Tempora/Nat1-0.txt` in the reference repo, and its
|
||
Monastic counterpart under `TemporaM/`), distinct from the individual
|
||
per-weekday octave pages (`Nat02`..`Nat09` = the ferias, keyed by day
|
||
offset from Christmas). Most years the Sunday does lose outright to a
|
||
Duplex-II-classis octave saint (St. Stephen/St. John/Holy Innocents,
|
||
whichever Dec 26–28 lands on) and only gets a bare commemoration — that's
|
||
what the earlier searches kept finding — but 2029-12-30 is a clean year
|
||
where the Sunday itself wins outright (`Dominica Infra Octavam
|
||
Nativitatis ~ Semiduplex Dominica minor`, no commemoration line at all),
|
||
so that's the date the content was pulled from. Benedictus antiphon:
|
||
"Dum médium siléntium" / "While all things were in quiet silence" — note
|
||
the source's own English translation genuinely omits the closing
|
||
"alleluia" that the Latin has; transcribed as-is, not smoothed over.
|
||
|
||
Method: rather than hunting for "clean years" one at a time, wrote a
|
||
script that iterates every day of a test year through this app's own
|
||
`resolveDay`, buckets each Sunday by which temporal id actually won, and
|
||
picks one real date per id straight from the app's own logic — then
|
||
cross-checked each date's *live* D.O. title for a saint collision before
|
||
trusting the content (a handful, mostly on privileged Eastertide/
|
||
post-Pentecost Sundays, needed a different year: this app's own
|
||
privileged-Sunday rule never lets a saint displace those Sundays, but the
|
||
live reference engine's calendar is bigger than this app's and does let
|
||
some by-date saint win there, e.g. St. Mark on Easter V some years, "Our
|
||
Lady of the Snows" — not modeled in this app at all — landing on
|
||
Post-Pentecost XI whenever the date lines up with Aug 5).
|
||
|
||
### Octave content — done (2026-08)
|
||
|
||
Was: `getDayCollects` skipped a `kind: 'octave'` commemoration entirely,
|
||
and every other office part (psalmody, hymn, chapter, Benedictus
|
||
antiphon) stayed keyed off the plain temporal day underneath the octave
|
||
— so an ordinary day within e.g. St. Lawrence's own octave showed nothing
|
||
of his at all, not even a label beyond the day-label line. Root cause:
|
||
found live (Tridentine 1910, 2026-08-12) that on a day like this, the
|
||
*whole* office — chapter/responsory/hymn/versicle, psalm antiphons,
|
||
Benedictus antiphon, day collect — comes from the octave's own feast, not
|
||
the plain temporal day; only a *weaker* commemorated saint on top of that
|
||
(St. Clare, that same date) gets a separate Ant+V/R+collect block of her
|
||
own. Fixed via `hours/resolve-common.ts`'s `resolveOfficeWinner`, used
|
||
everywhere `getDayCollect`/`getDayCollects`/`getBenedictusAntiphon`/
|
||
`getPsalmodyOverrideFor` used to key off `day.winner` directly — and
|
||
`getDayCollects` now renders a real Ant+V/R+collect bundle for a
|
||
sanctoral commemoration (`${propers}-commemoration.yml`, e.g.
|
||
st-clare-commemoration.yml) when authored, falling back to a *labeled*
|
||
bare collect (or a labeled "missing" placeholder) rather than the old
|
||
unlabeled one that just read "PRAYER … (translation pending)" with no
|
||
indication whose it was.
|
||
|
||
Remaining gap, now closed (2026-08): `resolveOfficeWinner` deliberately
|
||
only kicks in when `temporalCategory === 'ordinary-feria'` (the octave
|
||
day has no standing of its own) — the live-verified counterexample was
|
||
the Christmas Octave's own stack (Christmas + Stephen + John + Holy
|
||
Innocents, e.g. on Dec 30, `privileged-feria-minor`): the *temporal* day
|
||
itself keeps the office there ("Dominica Infra Octavam Nativitatis", `{ex
|
||
Proprio de Tempore}`), and each of the four octaves gets its own separate
|
||
"Commemoratio Octavæ …" Ant+V/R+collect block instead. Fixed by adding a
|
||
`kind === 'octave'` branch to `getDayCollects` (gated on
|
||
`day.temporalCategory !== 'ordinary-feria'`, the same condition
|
||
`resolveOfficeWinner` uses, so an ordinary octave day's already-rendered
|
||
primary content never gets a redundant repeat), plus one new
|
||
`${id}-octave-commemoration.yml` proper file per octave for the four
|
||
Christmas-stack ids (`christmas-day`, `st-stephen-protomartyr`,
|
||
`st-john-apostle`, `holy-innocents`), sourced from each Sancti file's own
|
||
`[Octava 3]` section in `../divinum-officium-reference` (Second Vespers'
|
||
own variant — the reference engine has three slightly different
|
||
Octava 1/2/3 texts per octave, roughly Vespers I/Lauds/Vespers II, but
|
||
this app's `getDayCollects` renders one shared block regardless of hour,
|
||
matching the pre-existing "one Orémus per commemoration, not chained"
|
||
simplification already noted above). The Holy Innocents' own English
|
||
Octava 3 block turned out to carry a mismatched antiphon in the reference
|
||
engine itself (the Common-of-Many-Martyrs "Isti sunt" text, not a
|
||
translation of the Latin antiphon printed beside it) — worked around by
|
||
sourcing the correct English from that same file's own `[Ant 3]`/
|
||
`[Versum 3]` sections instead (same Latin text, correctly paired
|
||
translation) — see that file's own comment. Live-verified end-to-end via
|
||
`tests/hours/vespers.test.ts`'s `2033-12-30` stacking case (all four
|
||
blocks resolve `verified` in both languages, not `missing`). This closes
|
||
the octave-content gap listed at the top of this file's "Open work"
|
||
section — no octave content work remains open.
|
||
|
||
## Benedictus antiphons for every sanctoral/temporal entry (in progress)
|
||
|
||
Full scope, corrected after actually enumerating it (an earlier pass here
|
||
under-counted): **109 sanctoral entries have `propers` set** (a saint id
|
||
that both a collect and an antiphon key off) and need a real Benedictus
|
||
antiphon; **94 more saints have `propers: null`** (no collect *or* antiphon
|
||
authored, ever) and are a separate, bigger prerequisite — see below. Plus
|
||
**~52 temporal entries** (every numbered Sunday/feria) need one too.
|
||
|
||
### Confirmed systemic bug, not just "unverified"
|
||
|
||
Every pre-existing `<saint>-antiphon.yml` file (there were 17, pulled during
|
||
the original sanctoral-content sessions, months before Lauds existed) was
|
||
sourced from the **Roman/secular rite's raw `[Ant 1]`**, not a live Monastic
|
||
1617 query. St. Lawrence's turned out wrong this way; going back and
|
||
correcting 3 more (Chair of St. Peter at Rome, St. Agnes, Conversion of St.
|
||
Paul) while working January confirms it's the same mistake every time, not
|
||
a one-off — **treat all pre-existing antiphon files as wrong until
|
||
re-verified against a live Monastic query**, not just flagged for a look.
|
||
|
||
### Method, worked out the hard way this pass
|
||
|
||
A saint's raw per-date file (`Latin/SanctiM/MM-DD.txt`, falling back to
|
||
`Latin/Sancti/MM-DD.txt`) is *not* a reliable shortcut — Monastic doesn't
|
||
always compute the Benedictus antiphon from a literal `[Ant 1]` field (major
|
||
feasts like Lawrence draw it from elsewhere entirely), and a shared date
|
||
with two co-assigned saints only has *one* real file, which may belong to
|
||
whichever saint reliably loses the day, not the one this app's calendar
|
||
means to ask about. What actually works: a live `command=prayLaudes` query
|
||
on the saint's own date, checked by eye against 2-3 different years for a
|
||
title that plausibly names the saint and stays consistent — full automation
|
||
kept producing false positives/negatives on exactly these edge cases, so
|
||
each result needs a human read, not just a script.
|
||
|
||
Also found along the way, now fixed: **this app's calendar and the
|
||
reference engine's own calendar don't always agree on dates.** St. Hilary
|
||
of Poitiers was assigned Jan 23 here; Divinum Officium's own calendar has
|
||
him fixed on Jan 14, confirmed directly against the raw `[Officium]` file
|
||
(Jan 23's own file is St. Raymond of Peñafort, not Hilary at all). The
|
||
original Jan-23 sourcing wasn't fabricated — 2029 is a real year where
|
||
Jan 14 lands on a Sunday and every day through the 22nd is already taken,
|
||
so Hilary genuinely transfers all the way to the 23rd *that one year* —
|
||
but the app had mistaken a transfer-year artifact for his real date. Moved
|
||
to Jan 14 in `sanctoral-calendar.yml`; his collect text (unaffected by
|
||
which day it's said on) re-confirmed identical; antiphon pulled fresh.
|
||
Worth a pass at some point checking whether any other already-authored
|
||
saint has the same kind of mismatch.
|
||
|
||
### Progress
|
||
|
||
- **January (9 total): done, all 9.** St. Marcellus I, Chair of St. Peter at
|
||
Rome, St. Paul the Hermit, Ss. Fabian and Sebastian, St. Agnes, Ss.
|
||
Vincent and Anastasius, Conversion of St. Paul, St. John Chrysostom, and
|
||
St. Hilary of Poitiers (moved to his real date, Jan 14, per direct
|
||
correction — see above).
|
||
- **February (7 total): done, all 7.** St. Agatha, St. Dorothea, St.
|
||
Apollonia, St. Scholastica (translated, source had no English),
|
||
St. Valentine, Chair of St. Peter at Antioch, St. Matthias (source's own
|
||
English existed but was jarringly modern next to everything else's
|
||
archaic register — reworded to match, marked `draft`).
|
||
- **March (6 total): done, 5 authored/corrected, 1 flagged.** St. Lucius I,
|
||
St. Thomas Aquinas, the Forty Holy Martyrs, St. Gregory the Great
|
||
(translated, source had no English — the "non Angli sed Angeli" antiphon),
|
||
St. Joseph (corrected). St. Frances of Rome left `missing`: Simplex rank,
|
||
and March 9 always falls within Lent every tested year (2026-2035) — she
|
||
structurally can never win her own day outright under this app's own
|
||
calendar rules, so there's no live-rendered antiphon to source at all.
|
||
- **April (8 total): done, all 8.** St. Francis of Paola, St. Leo I,
|
||
Ss. Tiburtius/Valerian/Maximus, St. George, St. Mark, St. Vitalis,
|
||
St. Robert, St. Peter Martyr.
|
||
- **May (13 total): done, all 13.** 3 corrected (Ss. Philip and James,
|
||
St. John Before the Latin Gate, the Apparition of St. Michael), 10
|
||
authored fresh (Athanasius, Monica, Stanislaus, Nereus/Achilleus/
|
||
Domitilla/Pancras, Boniface, Ubald, Peter Celestine, Urban I,
|
||
Eleutherius, John I). Urban I/Eleutherius/John I needed 2038 to find a
|
||
year not governed by Ascensiontide/Pentecost/Trinity/Corpus Christi.
|
||
- **June (16 total): done, all 16.** 5 corrected (Ss. Basilides/Cyrinus/
|
||
Nabor/Nazarius, the Nativity of St. John the Baptist, Ss. John and Paul,
|
||
Ss. Peter and Paul, Commemoration of St. Paul), 11 authored fresh
|
||
(Ss. Marcellinus/Peter/Erasmus, Ss. Primus and Felicianus, Barnabas
|
||
[reworded English, same register fix as Matthias], Anthony of Padua,
|
||
Basil the Great, Ss. Vitus/Modestus/Crescentia, Ss. Mark and Marcellian,
|
||
Ss. Gervase and Protase, Paulinus of Nola, William Abbot, Leo II).
|
||
- **July (9 total): done, all 9.** 1 corrected (Mary Magdalene), 8 authored
|
||
fresh (the Seven Brothers/Rufina/Secunda, Symphorosa and Her Seven Sons,
|
||
Margaret of Antioch, Apollinaris, James the Greater [reworded English,
|
||
same fix as Matthias/Barnabas], Anne, Nazarius/Celsus/Victor I/
|
||
Innocent I, Abdon and Sennen).
|
||
- **August (12 total): done, all 12.** 1 corrected (Peter's Chains), 9
|
||
authored fresh (Dominic, Ss. Cyriacus/Largus/Smaragdus, Assumption,
|
||
Bernard, Bartholomew [reworded English, same fix as Matthias/Barnabas/
|
||
James the Greater], Louis, Zephyrinus, Augustine), 2 already correct as
|
||
found (Finding of St. Stephen, the Decollation of St. John the Baptist
|
||
— both plain unmodified matches against the live query, no fix needed).
|
||
- **September (5 total): done, all 5.** 1 corrected (Nativity of the BVM),
|
||
4 authored fresh (Matthew [reworded English, same fix as Matthias/
|
||
Barnabas/James the Greater/Bartholomew], Ss. Cyprian and Justina,
|
||
Ss. Cosmas and Damian, Jerome).
|
||
- **October (7 total): done, all 7.** Francis of Assisi, Callistus I,
|
||
Luke [reworded English, same fix as Matthias/Barnabas/James the
|
||
Greater/Bartholomew/Matthew], Ss. Chrysanthus and Daria, Vigil of
|
||
Ss. Simon and Jude, Ss. Simon and Jude [same reworded-English fix],
|
||
Vigil of All Saints.
|
||
- **November (7 total): done, all 7.** 2 corrected (Cecilia, Andrew), 4
|
||
authored fresh (All Saints, Clement I, Catherine of Alexandria,
|
||
Silvester Abbot). Martin of Tours already matched the live query as
|
||
found, no change needed.
|
||
- **December (10 total): done, 9 authored/corrected, 1 flagged.** 2
|
||
corrected (Lucy), 6 authored fresh (Nicholas, Immaculate Conception
|
||
[minor English fix -- the source's own translation said "her Birth"
|
||
for a Conception antiphon, an obvious copy-paste leftover from the
|
||
Nativity-of-the-BVM formula this reuses; corrected, marked draft],
|
||
Damasus, Thomas of Canterbury). Thomas Apostle, Stephen Protomartyr,
|
||
John Apostle, and Holy Innocents already matched the live query as
|
||
found, no change needed. St. Bibiana left `missing`: Simplex rank, and
|
||
Dec 2 falls within Advent in every tested year 2026-2029 -- same
|
||
structural block as St. Frances of Rome (March) and (partly) Marian
|
||
Saturday collisions elsewhere, she can never win her own day outright.
|
||
|
||
**All 109 sanctoral entries with a `propers` id: done.** 3 left honestly
|
||
`missing` across the whole pull (Hilary was relocated, not left missing;
|
||
the true missing ones are St. Frances of Rome, St. Bibiana, and Ss.
|
||
Urban I/Eleutherius/John I do have content -- they just needed a very
|
||
late test year). Every pre-existing antiphon file that predated this
|
||
pass has now been checked against a live Monastic query; most needed
|
||
correction (confirmed Roman-rite sourced), a handful already matched.
|
||
- **All ~52 temporal entries: done, including `christmas-octave-sunday`**
|
||
(see the section above for method and its own correction).
|
||
- **94 `propers: null` saints: done (2026-08)** — see the dedicated
|
||
section below for the collect-authoring pass and the era-specific-
|
||
calendar discovery it took to do reliably. Benedictus antiphon: real
|
||
content for all of them except the deliberate `example-confessor` test
|
||
placeholder and Ss. Ursula and Companions (left honestly unset, no way
|
||
to verify a category of one — see her own file's comment) — 66 via a
|
||
shared Common (own dedicated section below); of the other 26 (own
|
||
proper collect), 21 share an existing Common (7 live-query-verified
|
||
directly, 14 more resolved straight from source once "unwinnable"
|
||
stopped being treated as a content blocker — see that section's own
|
||
note) and 5 have real unique text (one, Ss. Euphemia/Lucy/Geminian,
|
||
honestly marked `draft` rather than `verified` since it can never be
|
||
checked against a live rendering). P/T/S/N: done for all 94, and for
|
||
the whole rest of the calendar besides — see "`minorHoursCommon` gap
|
||
swept across the whole calendar" and "The last 2 duplex-majus+ P/T/S/N
|
||
gaps" below; every real saint on this app's calendar has real P/T/S/N
|
||
content now except Ss. Ursula and Companions, same reason as above.
|
||
|
||
### Bug found and fixed while pulling christmas-octave-sunday's antiphon
|
||
|
||
`data/calendar/temporal-categories.yml` had christmastide's own Sunday
|
||
coded `privileged-sunday` (Advent's tier — never displaced short of
|
||
Duplex-majus+, and even then only commemorated in return). Live-checking
|
||
all three Duplex-II-classis "Comites Christi" collision dates (St.
|
||
Stephen/John/Holy Innocents falling on the Sunday, plus St. Silvester,
|
||
plain Duplex) showed the exact reverse: the saint wins outright every
|
||
time, the Sunday only commemorated — `ordinary-sunday`'s rule, not
|
||
`privileged-sunday`'s. Fixed by reclassifying christmastide's `sunday` to
|
||
`ordinary-sunday` (the `feria` side, `privileged-feria-minor`, was
|
||
already correct and untouched). New tests in
|
||
`tests/calendar/december-sanctoral.test.ts` cover both directions: the
|
||
saint winning on the three verified collision dates, and St. Thomas
|
||
Becket (Semiduplex) still transferring off the Sunday rather than
|
||
winning, unchanged by the fix.
|
||
|
||
### Bug found while checking February's results — fixed (2026-08)
|
||
|
||
`hours/resolve-common.ts`'s `splitNamedAntiphon` always marked its output
|
||
`verified` regardless of the input status (`verifiedText()` hardcoded it) —
|
||
so a `draft`-marked antiphon (Scholastica's translation, Matthias's
|
||
reworded English, and every other reworded/translated antiphon from the
|
||
sanctoral pull) rendered as if fully verified everywhere it's actually
|
||
displayed — silently dropping the "unverified draft text" UI marker
|
||
(`src/ui/styles.css`'s `.text-draft`, a dashed underline + tooltip) — even
|
||
though the source YAML correctly said `draft`. Affected every split
|
||
antiphon across every hour (Prime, Terce, Sext, None, Compline, Lauds),
|
||
not just Scholastica/Matthias — pre-existing, just never surfaced until
|
||
content with a real `draft` status went through it.
|
||
|
||
Fixed by having `splitNamedAntiphon` take a real `ResolvedText` (not just
|
||
bare text) and carry its actual per-language status through to both the
|
||
incipit and full outputs. Callers that only ever had a plain
|
||
`Partial<Record<string,string>>` with no status of its own to begin with
|
||
(the plain weekday-default antiphons, Lauds psalmody overrides — neither
|
||
type tracks a status field, always implicitly verified by their own
|
||
live-checked file convention) now wrap with `verifiedText()` explicitly
|
||
at the call site instead of that being silently baked into
|
||
`splitNamedAntiphon` itself. `resolveMinorHourAntiphon` (Prime/Terce/
|
||
Sext/None's own override lookup) now returns a full `ResolvedText` too,
|
||
for the same reason. Verified end-to-end with St. Scholastica's own
|
||
draft-English antiphon (`tests/hours/resolve-common.test.ts`).
|
||
|
||
## Mechanism not built at all
|
||
|
||
- **Vespers ordo — built (2026-08)**: `src/hours/vespers.ts` now resolves a
|
||
full ordo (opening versicle, weekday/seasonal psalmody with the 115+116
|
||
join and 138 split, chapter/responsory/hymn/versicle bundle with
|
||
Advent/Lent/Passiontide/Paschaltide overrides, Magnificat with weekday-
|
||
default antiphon fallback, day-collects) off `resolveEveningDay`, so
|
||
First/Second Vespers precedence and the cross-day commemoration
|
||
(`commemorationOf` — "the loser is commemorated unless the loser is
|
||
simplex", live-verified) both apply before any part runs. See
|
||
`data/hours/vespers.yml`'s header and `tests/hours/vespers.test.ts`.
|
||
|
||
### Matins ordo — mechanism built, small content slice (2026-08)
|
||
|
||
`src/hours/matins.ts` was the last stub hour. Built as a genuinely different
|
||
shape from every other hour: no static `data/hours/matins.yml` parts list —
|
||
the ordo is assembled programmatically per day, since the real structure (1
|
||
nocturn on a plain ferial day, 3 on a Sunday or Duplex-and-higher feast,
|
||
with a *variable* number of readings) doesn't fit the static-array pattern
|
||
every other hour uses.
|
||
|
||
**Explicit, direct-instruction departures from the historical office** (see
|
||
memory `vu-not-a-reconstruction`/`vu-matins-design` — this app was never
|
||
meant to reconstruct any one historical form, Matins least of all):
|
||
- **Nocturn 1 is always the user's own continuous scripture-reading plan**
|
||
(`src/propers/bible-plan.ts`, sourced from a personal TSV outside this
|
||
repo), never the historical per-day lectionary — a variable number of
|
||
readings per day, and deliberately *not* the Rule of Benedict's own
|
||
"summer" reading contraction (Low Sunday–1st Sunday of November): this
|
||
app reads in full year-round, live-confirmed the reference engine's own
|
||
Monastic 1617 data does apply that contraction (`monastic.pl`'s
|
||
`brevis_monastic`/`legend_monastic`) before deciding not to reproduce it.
|
||
- **Recombine, don't reproduce the historical lesson split**: where the
|
||
source splits one continuous work across several numbered lessons purely
|
||
to fill a fixed lesson-count slot, this app joins them into one reading
|
||
(`src/propers/octave-readings.ts`'s `resolvePassages`, now reused outside
|
||
the octave-day store it was built for — extended via the new
|
||
`src/propers/nocturn-readings.ts`). Split only kept where the underlying
|
||
source genuinely changes (e.g. a Gospel pericope vs. the homily on it).
|
||
- **Nocturn 3 gated at Duplex-and-higher**, plus every Sunday
|
||
unconditionally — the user's own chosen threshold (real Monastic 1617 is
|
||
more permissive), reusing the already-existing `isDoubleOrHigher` (which
|
||
already means rank ≥ `duplex`, so no new rank helper was needed).
|
||
- **Generous multi-source reading inclusion, not winner-takes-all**: every
|
||
commemorated saint (not just the office winner) and every active octave
|
||
contributes its own Nocturn 2/3 reading when authored — mirrors
|
||
`getDayCollects`'s existing "one collect per commemoration" pattern,
|
||
applied to readings. Wired `src/propers/octave-readings.ts`'s
|
||
`getOctaveReading` into a real caller for the first time — it had zero
|
||
callers anywhere in the codebase before this, clearly built ahead of
|
||
Matins and left unwired until now.
|
||
- **Gospel sourcing has exactly two allowed sources**: the user's own plan
|
||
(flagged via `isGospel`, book-derived — confirmed the user's plan never
|
||
assigns one on a Sunday, a deliberate editorial choice, not a gap) and
|
||
the day's own genuine *proper* Gospel+homily. **A Common-of-Saints
|
||
fallback Gospel is never used** — `nocturn-readings.ts` has no
|
||
Common-fallback mechanism at all (unlike `collectCommon`/
|
||
`benedictusCommon` elsewhere), so the exclusion falls out of the store's
|
||
own shape rather than needing special-case code.
|
||
- **Invitatory (Ps 94) framed like an ordinary antiphoned psalm** — one
|
||
opening (incipit-or-full), one full repeat after — not the historical
|
||
interspersed-refrain shape (confirmed live: real practice repeats the
|
||
antiphon between every verse group). Direct instruction, not an
|
||
oversight.
|
||
- **Responsories matched loosely by scriptural book**, not exact citation —
|
||
per the user's own description of how they actually worked historically.
|
||
New seeded pool, `data/hours/matins-responsories-by-book.yml` +
|
||
`src/propers/matins-responsories.ts` (currently just `isa`, from a live
|
||
Advent-ferial query — a real seed, not a full pool).
|
||
|
||
**Sunday's fixed 12-psalm/3-canticle psalmody** (`data/hours/matins-sunday-
|
||
antiphons.yml`) — Nocturn 1 = Ps 20-21/22-23/24-25, Nocturn 2 = Ps
|
||
26-27/28-29/30-31 (Rule of Benedict ch. 18's own arrangement, distinct from
|
||
Roman Matins' 9-psalm Sunday scheme), Nocturn 3 = 3 OT canticles (Isa
|
||
33:2-10, Isa 33:13-18, Sir 36:14-19) under one shared antiphon. Transcribed
|
||
from the reference engine's own Monastic-labeled block (`Psalterium/Psalmi/
|
||
Psalmi matutinum.txt`'s `[Daym0]`), then live-verified end-to-end
|
||
(`command=prayMatutinum`, 2026-09-06) — psalm numbers, canticle citations,
|
||
and the 12-lesson/Te Deum sequence all matched exactly. Real gap found
|
||
along the way: the reference engine's own UI flags Monastic 1617 Matins as
|
||
"still incomplete and under construction" — the pieces actually queried
|
||
checked out regardless, but worth remembering if a future pull comes back
|
||
looking wrong.
|
||
|
||
**Real content authored for two clean proof dates** (one per branch, per
|
||
this project's usual "mechanism first, minimal live-verified proof, content
|
||
later" build order):
|
||
- **Ferial**: 2026-12-15 (Tuesday of Advent III) — the user's own plan's
|
||
"Isa 36-37, Wis 14" (citation-only for now; full Vulgate/Douay-Rheims
|
||
import is a deferred separate project, see below), matched against the
|
||
seeded Isaiah responsory pool.
|
||
- **Sunday**: 2026-09-06 ("14th Sunday after Trinity", `post-pentecost-15`
|
||
— the Trinity-counted display label and the Pentecost-offset storage id
|
||
are a fixed, already-computed one-off relationship, not something new
|
||
built here) — Nocturn 1 from the user's plan ("Tob 1-2, Sir 45",
|
||
citation-only), Nocturn 2 a real, fully-translated patristic reading
|
||
(St. Gregory the Great, *Moralia in Job*, Bk. 9 — live-transcribed, both
|
||
languages), Nocturn 3 the day's own proper Gospel (Luke 7:11-16, the
|
||
raising of the widow's son at Naim) plus St. Augustine's homily on it
|
||
(Sermon 44), also fully transcribed. Real seam flagged, not papered over:
|
||
this Sunday's *real* historical Nocturn 1 is the Book of Job (confirmed
|
||
live) — Nocturn 2's Job commentary is kept as-is even though this app's
|
||
own Nocturn 1 no longer reads Job that week, since it's real, valuable,
|
||
season-proper patristic content, not tied to any specific saint.
|
||
- Two Isaiah chapters actually populated in `src/data/scripture/` (33,
|
||
live-transcribed for the Sunday canticles) — small, real progress against
|
||
the eventual bulk Bible import, not just placeholders.
|
||
|
||
Verified end-to-end: `tests/hours/matins.test.ts`, both branches, resolving
|
||
real dates through the actual `resolveOrdo` mechanism (psalm/canticle
|
||
counts, Te Deum presence, reading citations and statuses, Gospel flagging
|
||
and its Common-fallback exclusion, responsory matching) — not just checking
|
||
the YAML files exist. `npm test` (359 tests) and `npm run build` both pass.
|
||
|
||
### Reading-pool redesign — done (2026-08)
|
||
|
||
**Corrected a miscommunication in the original mechanism build above**: readings are now
|
||
pool-assembled, not fixed-slotted. Every source that can contribute for a day — the office
|
||
winner, every commemorated saint, a transferred-in feast (already folded into `day.winner`
|
||
by `resolveDay`, no separate lookup needed), every active octave, the plain temporal id, and
|
||
the user's own scripture-plan reading — is gathered into one ordered pool
|
||
(`buildReadingPool` in `src/hours/matins.ts`), then sliced into however many nocturns the
|
||
day's psalmody has (`distributeIntoNocturns`, ceiling division, pool order preserved). No
|
||
reading kind is pinned to a specific nocturn anymore — scripture is no longer reserved for
|
||
Nocturn 1, patristic content no longer reserved for Nocturns 2-3.
|
||
|
||
The 1-vs-3-nocturn psalmody gate (Duplex+/Sunday) is unchanged — it governs psalm/canticle/
|
||
Te Deum structure only. It no longer limits which days get patristic content authored or
|
||
shown: a sub-Duplex ferial day's single nocturn now pools in patristic/hagiographic content
|
||
whenever it's been sourced, alongside the scripture-plan reading — direct instruction, "we
|
||
want patristic readings for every saint where we can source one, not just duplex+."
|
||
|
||
`src/propers/bible-plan.ts`'s `getBiblePlanReadings` is now dual-keyed: most rows still key
|
||
off `(temporalId, weekday)`, but the source TSV's Dec 25 - Jan 13 stretch keys off a fixed
|
||
`MM-DD` calendar date instead (read straight through regardless of which temporal Sunday
|
||
governs the day) — a date can match both keys at once (e.g. a Sunday after Epiphany landing
|
||
inside Jan 1-13), and both are pooled rather than one overriding the other. No calendar-date
|
||
rows are authored yet (mechanism only); see memory `vu-matins-design` for the full design
|
||
writeup. `npm test` (359 tests, unchanged — the two proof dates still resolve the same
|
||
content, just via the new pool/distribute path) and `npm run build` both pass.
|
||
|
||
**Not done in this pass — explicitly deferred, not silently dropped:**
|
||
- Full Sunday/feast-calendar patristic-reading authoring — now framed as
|
||
"every saint where a reading can be sourced," not just the ~75 Duplex+
|
||
saints that get their own Nocturn 3 slot (see the reading-pool redesign
|
||
above: content is no longer rank-gated, only nocturn *count* is). **Update
|
||
(2026-08): the post-Pentecost season side of this is now done — see
|
||
"Temporal-cycle nocturn-readings sweep" below.**
|
||
- The per-book responsory pool beyond its one seeded book (`isa`).
|
||
- The bulk Vulgate (Latin) + Douay-Rheims (English) import into
|
||
`src/data/scripture/` — in scope for the project long-term, explicitly
|
||
deferred past this pass per direct instruction.
|
||
|
||
### Temporal-cycle nocturn-readings sweep — post-Pentecost season done (2026-08)
|
||
|
||
The user noticed 2026-08-23's Matins (a plain Sunday, `post-pentecost-13`) was missing its
|
||
Nocturn 2/3 patristic reading entirely. Root cause: `post-pentecost-15` (above) was the *only*
|
||
temporal id with any `nocturn-readings` content authored — every other ordinary Sunday/week
|
||
across the whole year had none, a gap distinct from (and larger than) the sanctoral-saint gaps
|
||
tracked elsewhere in this file.
|
||
|
||
Scope turned out bounded: `calendar/temporal-id.ts`'s `resolveTemporalId` maps every day in a
|
||
liturgical week (Sunday and its ferias alike) to the same id, and `hours/matins.ts`'s
|
||
`buildReadingPool` already keys nocturn-readings lookup by that shared id — so **one file per
|
||
temporal id covers its entire week**, not one file per date. Authored all 23 remaining
|
||
`post-pentecost-NN` ids (01-14, 16-24; `-15` already existed) from the reference engine's own
|
||
`Tempora/PentNN-0.txt` (Latin) / `English/Tempora/PentNN-0.txt` files:
|
||
|
||
- **`post-pentecost-01` (Trinity Sunday, Duplex I classis)**: has its own full 9-lesson proper
|
||
office, unlike every other Sunday in the season — Nocturn 2 (St. Fulgentius on the Trinity)
|
||
plus *two* separate Nocturn 3 readings (St. Gregory Nazianzen's homily on Matt 28, and a
|
||
distinct "Commemoratio Dominicæ" — St. Augustine's homily for the displaced First Sunday
|
||
after Pentecost's own Gospel, Luke 6:36-42, which the Duplex I classis feast bumps into a
|
||
commemoration rather than dropping — consistent with this project's generous-commemorations
|
||
principle).
|
||
- **`post-pentecost-02` through `-11`**: each has real, unique proper content for *both*
|
||
Nocturn 2 (a patristic/theological excerpt, one continuous author per week — St. John
|
||
Chrysostom, St. Augustine, Pius XI, St. Ambrose, St. Jerome, St. Gregory the Great, etc.) and
|
||
Nocturn 3 (that week's own Gospel pericope + homily). `-11` is the last week in the source
|
||
with its own proper Nocturn 2; every week after it has Nocturn 3 only.
|
||
- **`post-pentecost-12` through `-14`, `-16` through `-24`**: Nocturn 3 only (no proper
|
||
Nocturn 2 in the source from here on — the historical Nocturn 1/2 for these weeks is a
|
||
continuous Old Testament book reading with commentary that isn't written out per-Sunday in
|
||
the static files the way the Gospel+homily slot is; sourcing that properly would need
|
||
live-CGI queries per date, out of scope for this pass, left as a known gap rather than
|
||
guessed at). Every week's Lectio7 in the source is a bare "Et réliqua" Gospel incipit, not a
|
||
full pericope — per this project's standing convention, not authored as its own Gospel
|
||
reading; only the homily (Lectio7's own opening paragraph plus Lectio8-9, one continuous
|
||
work) is authored, `isGospel: false`.
|
||
- **Responsory reuse discovered and followed**: the source itself cross-references the *same*
|
||
two responsories for the Nocturn 2 and Nocturn 3 homily slots in nearly every week
|
||
(`@Tempora/Pent01-2:ResponsoryN` for Nocturn 2, `@Tempora/Pent01-0:Responsory8` — "Duo
|
||
Séraphim" — for the Nocturn 3 homily), rather than a fresh responsory each week. Resolved
|
||
those cross-references once (from `Pent01-2`/`Pent01-3`/`Pent01-0`) and reused the resulting
|
||
text verbatim across every file that points at them, rather than re-deriving per week.
|
||
- **A genuine authoring bug found and fixed mid-pass**: three of the first Nocturn-3-only
|
||
files (`post-pentecost-13`, `-14`, `-16`) initially omitted the homily's own opening
|
||
paragraph — it lives inside the source's `[Lectio7]` section (right after the bare Gospel
|
||
incipit), not `[Lectio8]`, and an early extraction pass started from Lectio8 by mistake.
|
||
Caught by re-diffing each file's opening sentence against the source before moving on;
|
||
fixed by prepending the missing paragraph (Latin + English) to all three. Worth remembering
|
||
for any future pass over this same source shape: always check that a Nocturn 3 homily
|
||
reading's first sentence matches Lectio7's own prose, not Lectio8's.
|
||
|
||
Live-verified via `npx tsc --noEmit`, `npm test` (452 tests, up from 447 — 5 new cases in
|
||
`tests/hours/matins.test.ts`'s new "post-Pentecost season nocturn-readings sweep" describe
|
||
block, including a spot-check of 2026-08-23 itself), and `npm run build`. No fixture
|
||
collisions from this pass (pure content addition under already-existing ids, no calendar
|
||
changes).
|
||
|
||
**Not done in this pass — explicitly deferred, not silently dropped:**
|
||
- The rest of the temporal cycle: Advent (`advent-1..4`), Christmastide
|
||
(`christmas-octave-sunday`), post-Epiphany (`post-epiphany-1..6`), Septuagesima through Lent
|
||
(`septuagesima`, `sexagesima`, `quinquagesima`, `lent-1..4`), Passiontide/Holy Week
|
||
(`passion-sunday`, `palm-sunday`), and Eastertide (`easter-sunday`, `easter-octave`,
|
||
`easter-3..6`, `sunday-after-ascension`, `pentecost-sunday`) — same shape of gap, same
|
||
pipeline, just not yet worked through season by season.
|
||
- Sourcing real Nocturn 2 content for `post-pentecost-12` onward via the Pentecost-offset id
|
||
itself — **resolved differently, not left open**, see "Calendar-month/week nocturn-readings
|
||
import" immediately below: that content turned out to be keyed by civil calendar month, not
|
||
Pentecost offset, at the source, so it needed a second id scheme, not a live-CGI lookup.
|
||
- Ferial-specific (non-Sunday) content within a post-Pentecost week — not applicable here,
|
||
since the whole week already shares one temporal id/file by design (see above); flagged only
|
||
so a future reader doesn't go looking for `post-pentecost-13-tuesday.yml`-style files that
|
||
were never meant to exist.
|
||
|
||
### Calendar-month/week nocturn-readings import — done (2026-08)
|
||
|
||
Follow-up to the post-Pentecost sweep above: the user asked about the "Nocturn 3 only, from
|
||
`post-pentecost-12` onward" gap it left open, correctly guessing that the reference engine's
|
||
real Nocturn 2 content for those later Sundays lives in older, month-labeled files rather than
|
||
being genuinely absent.
|
||
|
||
Confirmed: `Tempora/08N-0.txt` through `11N-0.txt` (N = 1-5, August-November, ~20 files) — the
|
||
old "Scriptura occurrens" reading cycle, "Dominica I-V Augusti/Septembris/Octobris/Novembris."
|
||
Each has `[Lectio1]`-`[Lectio3]` (Nocturn 1 scripture — **not imported**, the user's own
|
||
bible-plan store already owns that slot) and `[Lectio4]`-`[Lectio6]` (Nocturn 2 patristic
|
||
commentary — the actual target).
|
||
|
||
**The real complication, and why this needed a mechanism addition, not just content files**:
|
||
which month-week file governs a given Sunday is *not* a fixed `post-pentecost-NN`
|
||
correspondence — it's computed straight from the civil calendar (first Sunday of each month,
|
||
with irregularities), and shifts every year with Easter's date. Ported the reference engine's
|
||
own `Date.pm::monthday()` sub faithfully into `src/calendar/month-week-id.ts` (`monthWeekId`),
|
||
using this app's pre-1955/Tridentine rubric baseline (`Date.pm`'s own `$modernstyle = false`):
|
||
no 1960-rubric October week-III vanishing (skipped entirely, since that whole adjustment is
|
||
modernstyle-gated); November's backward-from-Advent renumbering for week > 0 *does* still
|
||
apply (that part of the source isn't modernstyle-gated). Live-verified against the localhost
|
||
CGI instance across 3 different years (2026, 2027, 2028) including the August 1st-falls-on-a-
|
||
Saturday skip case and November's real week-II-vanishes case (2026: Nov 1/8/15/22 render "I.
|
||
/III./IV./V. Novembris" — II is genuinely skipped that year, matching the ported algorithm
|
||
exactly).
|
||
|
||
Wired in as a **third, independent pooling source** in `hours/matins.ts`'s
|
||
`nocturnReadingIds`/`buildReadingPool` — `month-week-<id>` (e.g. `month-week-081`), pooled
|
||
alongside (not replacing) the ordinary `temporalId`-keyed lookup, same dual-key precedent as
|
||
`propers/bible-plan.ts`'s Dec25-Jan13 calendar-date stretch. All 20 ids authored as
|
||
`data/propers/nocturn-readings/month-week-<MMN>.yml`; each file's 3 lessons are one continuous
|
||
work (recombined per the standing rule), so one `NocturnReading` per file. `month-week-092`
|
||
turned out to cite the exact same Moralia-in-Job passage already authored for
|
||
`post-pentecost-15` — reused that existing translation rather than re-transcribing it.
|
||
|
||
Live-verified via `npx tsc --noEmit`, `npm test` (460 tests, up from 452 — a new "calendar-
|
||
month/week nocturn-readings import" describe block in `tests/hours/matins.test.ts` spanning
|
||
2026-2028 to prove the date-driven lookup, not a hardcoded id), and `npm run build`. No fixture
|
||
collisions (pure content + one new pooling source, no calendar changes).
|
||
|
||
**Not done in this pass**: the same month-week treatment for whatever Sundays this pass didn't
|
||
reach (there are none within Aug-Nov — all 20 files are authored); the rest of the temporal
|
||
cycle (Advent through Eastertide, per the post-Pentecost sweep's own "not done" list above)
|
||
remains open and is unrelated to this particular gap.
|
||
|
||
**Ordering bug found and fixed same day**: pooling a `temporalId`'s own file (Nocturn 3 only,
|
||
for a late post-Pentecost Sunday) alongside a separately-sourced `month-week-<id>` file
|
||
(Nocturn 2 only) put the Nocturn 3 reading ahead of the Nocturn 2 reading in the pool — the two
|
||
sources were being concatenated in `nocturnReadingIds` set order, not grouped by each reading's
|
||
own `nocturn` tag, so `distributeIntoNocturns`'s front-light positional slotting (1st pool
|
||
reading -> Nocturn 1, 2nd -> Nocturn 2, rest -> Nocturn 3) put the Nocturn-3-tagged reading into
|
||
the Nocturn 2 slot and vice versa. Confirmed live on the date that surfaced the whole month-week
|
||
gap in the first place (2026-08-23, `post-pentecost-13` + `month-week-084`): St. Gregory's
|
||
Moralia commentary (tagged `nocturn: 2`) was showing up after St. Augustine's homily (tagged
|
||
`nocturn: 3`) instead of before it. Fixed in `buildReadingPool` (`hours/matins.ts`) by bucketing
|
||
every contributing id's readings by `reading.nocturn` and emitting the buckets in ascending
|
||
numeric order — not hardcoded to `[2, 3]`, since Ember days' own nocturn-readings files use
|
||
`nocturn: 1` for their single-nocturn structure, and an initial `[2, 3]`-only version of this
|
||
fix silently dropped those (caught by `tests/calendar/ember-days.test.ts` failing). Within each
|
||
bucket, id order (and each file's own reading order) is preserved, matching this pool's usual
|
||
priority rule. `npm test` green at 460 tests — no fixture changes needed, since every
|
||
previously-authored id's own file already had its readings correctly ordered internally; only
|
||
the cross-source case this pass introduced was actually broken.
|
||
|
||
### Nocturn-readings responsory marker fix — done (2026-08-23)
|
||
|
||
Every `data/propers/nocturn-readings/*.yml` responsory with a `responsory:` field (53 files:
|
||
all 24 `post-pentecost-*`, all 20 `month-week-*`, the 6 Ember-day files, plus `st-monica`,
|
||
`st-marcellus-i`, `apparition-of-st-michael`) started straight into the Latin/English text with
|
||
no leading marker, unlike `data/propers/octave-readings/*.yml`'s own convention of an explicit
|
||
`℟.`/`℣.` at the start of each line. User flagged this via the Augustine reading on
|
||
2026-08-23's date and explicitly chose to fix it in content, not by injecting a marker at
|
||
render time. Given each file already uses plain `V.`/`R.` (not the `℣.`/`℟.` glyphs) for the
|
||
versicle and the mid-responsory repeat, `R. ` was prepended to just the opening `la`/`en` text
|
||
of each responsory to match that same plain-letter convention already used internally by these
|
||
same files — not a switch to octave-readings' Unicode-glyph convention, which remains a
|
||
separate, still-open reconciliation between the two stores if it's ever wanted. Mechanical,
|
||
scripted edit (53 files, idempotent — skips any responsory already starting `R. `), verified no
|
||
unexpected format by having the script flag anything it couldn't parse (none found). `npm test`
|
||
(460), `tsc`, and `npm run build` all still green — pure content edit, no code/mechanism change.
|
||
|
||
### Pre-1955-but-post-1617 saints' Matins readings — done (2026-08)
|
||
|
||
Resolved the former "mechanism gap" above into a concrete, scoped content pass — no new
|
||
mechanism was actually needed, since `nocturn-readings.ts`/`buildReadingPool` already accept
|
||
content from any source once authored (see the reading-pool redesign above); this was a
|
||
sourcing gap, not a design gap.
|
||
|
||
**Method**: read the reference engine's own files directly on disk (`../divinum-officium-reference`),
|
||
not live queries — `web/www/horas/Latin/Sancti/MM-DD.txt` is the Tridentine track (Sancti, no
|
||
`M`), `web/www/horas/Latin/SanctiM/MM-DD*.txt` the Monastic track. A saint with a `Sancti/`
|
||
file but no `SanctiM/` file at all (any suffix) never existed in Monastic 1617, confirming the
|
||
pre-1955-but-post-1617 case. **Critical wrinkle found and now memory'd** (see memory
|
||
`vu-reference-engine-file-layout`, `vu-matins-pre1955-saints`): the plain, unsuffixed
|
||
`Sancti/MM-DD.txt` is very often the *wrong* saint — the general/Tridentine calendar keeps
|
||
piling later canonizations onto the same date slot in that file, displacing the older saint
|
||
into a suffixed sibling file (`01-23o.txt`, `03-04cc.txt`, etc.) or, in one case (St. Paul the
|
||
Hermit), an entirely different date. Every one of the ~52 candidates was individually verified
|
||
by `[Officium]` match, not assumed from the filename.
|
||
|
||
**Scope, computed against `sanctoral-calendar.yml`'s 204 saint-day entries**: 52 pre-1955-only
|
||
candidates (`Sancti/` present, no `SanctiM/` at all, any suffix); of these, 43 had real proper
|
||
lesson content to author, 9 turned out to be pure Common-of-X with no proper text in the
|
||
source at all (`st-lucius-i`, `vigil-of-st-james`, `st-christina`, `vigil-of-st-bartholomew`,
|
||
`ss-twelve-brothers`, `vigil-of-ss-simon-and-jude`, `st-saturninus`, `st-sabbas`,
|
||
`vigil-of-st-thomas` — correctly left unauthored, not a gap). All 43 authored as
|
||
`src/data/propers/nocturn-readings/<id>.yml`, same shape and combine-where-sensible rule as
|
||
the two original proof dates. A few readings reuse already-verified text from
|
||
`post-pentecost-15.yml` where the source file itself cross-references another day's lessons
|
||
(e.g. `st-monica`'s Nocturn 3 Gospel, the same Luke 7:11-16 pericope). A handful of bare
|
||
Gospel pericopes were left unauthored where the source gives only an incipit ("Et réliqua")
|
||
and no full text was available to transcribe (flagged per-file, not silently dropped) — same
|
||
deferral as the bulk Vulgate/Douay-Rheims import. One saint (`st-anicetus`) has an
|
||
English-source transcription bug (Latin duplicated under the English heading) — this
|
||
project's own translation used instead, marked `draft`.
|
||
|
||
`npm test` (365 tests, all passing — none of the new saints are in a test table yet, same as
|
||
the bible-plan bulk conversion) and `npm run build`/`tsc --noEmit` both pass.
|
||
|
||
**Not done in this pass**: the larger pool of saints that *do* have a Monastic 1617
|
||
(`SanctiM/`) track — same authoring shape, separate/older backlog item (see priority §2 above).
|
||
|
||
### SanctiM (Monastic 1617)-track saints' Matins readings — done, all 12 months (2026-08)
|
||
|
||
The complementary pool to the pre-1955 pass above: saints with a `SanctiM/MM-DD*.txt` file.
|
||
**Method finding, worth recording**: a `SanctiM/MM-DD.txt` file almost always turns out to be
|
||
a thin *rule override* (`@Sancti/MM-DD` plus tweaks to psalmody/lesson-count/antiphons), not
|
||
independent content — when it doesn't override the Nocturn 2/3 `[LectioN]` sections, that
|
||
saint's Monastic-office vita reading is **the same text as the Tridentine `Sancti/MM-DD.txt`
|
||
file**, sourced exactly as in the pre-1955 pass above. Only a real Monastic-only saint (one
|
||
with no `Sancti/MM-DD*.txt` at all, e.g. `st-maurus-abbot`, who isn't on the general
|
||
Tridentine calendar) needs content read directly out of the `SanctiM/` file itself.
|
||
|
||
**A second wrinkle, on top of the pre-1955 pass's "don't trust the plain file" one**: the
|
||
"does a `SanctiM/MM-DD*.txt` exist for this date" scope check has to be done **per saint, not
|
||
per date** — a shared calendar date (two ids mapped to the same `MM-DD`) can have a `SanctiM`
|
||
file for one saint's own Tridentine slot while the other saint has no Monastic office at all.
|
||
Naively globbing "any `SanctiM/MM-DD*.txt` exists for this date" over-counts: found this by
|
||
hand for Jan 18 (`chair-of-st-peter-at-rome` has `SanctiM/01-18.txt`; `st-prisca`, sharing the
|
||
date, does not — she was actually a missed pre-1955-pool candidate) and Jan 14
|
||
(`st-hilary-of-poitiers` has `SanctiM/01-14.txt`; `st-felix-presbyter`, sharing the date via
|
||
`Sancti/01-14cc.txt`, does not — likewise a missed pre-1955-pool candidate). Both were authored
|
||
this pass using the pre-1955 pattern instead. **The 52/117 pre-1955/SanctiM split recorded
|
||
above is therefore an undercount on the pre-1955 side** by at least these two; the true
|
||
per-saint (not per-date) recomputation used for this pass found **152 ids with a genuine,
|
||
correctly-suffix-matched `SanctiM` file** — see memory `vu-matins-pre1955-saints` for the
|
||
running correction note.
|
||
|
||
**A third finding**: a `Sancti`/`SanctiM` date landing inside the Christmas or Epiphany octave
|
||
(e.g. `st-telesphorus` Jan 5, `st-hyginus` Jan 11 — both early popes commemorated on octave
|
||
days) can have its entire Nocturn 2/3 given over to octave-of-the-season homiletic content
|
||
(a Nativity/Epiphany sermon), with **no proper vita text for the saint at all** in either
|
||
track. Correctly left unauthored for these two — not a gap, same category as the pre-1955
|
||
pass's 9 pure-Common exceptions, just a different underlying reason (octave-absorbed, not
|
||
Common-of-X).
|
||
|
||
**January done (11 saints, 2 left unauthored per the octave finding above)**:
|
||
`st-hilary-of-poitiers`, `st-felix-presbyter`, `st-maurus-abbot` (Monastic-only; no English
|
||
source translation exists in the reference engine at all for this file, so `en` is this
|
||
project's own translation, marked `draft`), `st-anthony-abbot`, `chair-of-st-peter-at-rome`
|
||
(Nocturn 2 patristic + Nocturn 3 genuine Gospel-and-homily, both authored),
|
||
`st-prisca`, `ss-fabian-and-sebastian`, `st-agnes`, `ss-vincent-and-anastasius`,
|
||
`conversion-of-st-paul`, `st-john-chrysostom`. `st-telesphorus`, `st-hyginus` (octave-absorbed,
|
||
see above) were originally left unauthored here — **since authored, see "Octave-absorbed and
|
||
Vigil-only readings" below.** `npm test`/`tsc --noEmit` both pass; none of these are in a test
|
||
table yet, same as the pre-1955 pass.
|
||
|
||
**February done (8 saints, 1 left unauthored as Common-only, 1 vigil left unauthored as
|
||
Common-only)**: `st-ignatius-of-antioch` (Nocturn 2 patristic vita + Nocturn 3 homily-only,
|
||
bare Gospel incipit not transcribed per convention), `st-agatha`, `st-dorothea` (turned out to
|
||
be a missed pre-1955-pool candidate, not a real SanctiM case — the SanctiM file for this date
|
||
belongs to St. Titus, who shares Feb 6 and isn't on vu's own calendar), `st-romuald`,
|
||
`st-scholastica`, `st-simeon` (also a missed pre-1955-pool candidate — the SanctiM file at this
|
||
date is unrelated content for St. Bernadette Soubirous, not Simeon),
|
||
`chair-of-st-peter-at-antioch` (Nocturn 2 patristic + Nocturn 3 homily-only), `st-matthias`
|
||
(Nocturn 3 homily-only; Nocturn 2's own Lectio4-6 falls back to Common of an Apostle, not
|
||
proper text). Left unauthored: `st-valentine` (Lectio93 itself is `@Commune/C2:Lectio4` —
|
||
Common-only, no proper text in the source at all) and `vigil-of-st-matthias` (Vigilia rank,
|
||
`ex C1v`, no Lectio content at all — Common-only). `npm test`/`tsc --noEmit` both pass.
|
||
|
||
**A fourth SanctiM-pass finding, from this month**: a bare Gospel incipit ("Et réliqua") with
|
||
a genuine patristic homily following it (`st-ignatius-of-antioch`'s Nocturn 3,
|
||
`chair-of-st-peter-at-antioch`'s Nocturn 3, `st-matthias`'s Nocturn 3) is now authored as the
|
||
homily alone, `isGospel: false`, not combined with the bare incipit line and not marked
|
||
`isGospel: true` — cleaner than `chair-of-st-peter-at-rome`'s January file, which included the
|
||
incipit line under `isGospel: true` despite not having the full pericope text. That January
|
||
file is left as-is (not wrong, just an earlier, less clean judgment call) — this convention
|
||
applies going forward.
|
||
|
||
**March done (6/6 saints)**: `st-thomas-aquinas`, `st-frances-of-rome`, `forty-holy-martyrs`,
|
||
`st-gregory-the-great` (Lectio7-9's Gospel+homily is generic Chair-of-Peter content copied
|
||
verbatim into this file, unrelated to Gregory — same lessons as
|
||
`chair-of-st-peter-at-rome.yml`'s own sourcing, not proper to this feast, so not authored
|
||
here), `st-joseph`, `st-benedict` (Duplex I class patronal feast — the richest SanctiM case yet:
|
||
Nocturn 2 sourced from `Sancti/03-21.txt`'s own vita per the usual pattern, but Nocturn 3 is
|
||
**genuinely Monastic-only content not found in `Sancti/` at all** — a St. Peter Damian sermon
|
||
preached specifically for Benedict's own feast; no `English/SanctiM/03-21.txt` exists in the
|
||
reference engine, so `en` for that reading is this project's own translation, marked `draft`).
|
||
`npm test`/`tsc --noEmit` both pass.
|
||
|
||
**April done (9/9 saints)**: `st-leo-i`, `ss-tiburtius-valerian-and-maximus` (missed
|
||
pre-1955-pool candidate — the date's only SanctiM file belongs to St. Justin, who shares Apr 14
|
||
and isn't sourced this way), `st-george` (a reference-engine data bug: `Sancti/04-23.txt`'s own
|
||
`[Lectio94]` Latin section actually contains English prose — same kind of bug as `st-anicetus`
|
||
in the pre-1955 pass — so `la` here is this project's own translation, marked `draft`;
|
||
`en` is the source text, genuinely present just filed under the wrong heading, `verified`),
|
||
`st-mark`, `ss-cletus-and-marcellinus`, `st-vitalis`, `st-robert` (genuinely Monastic-only, no
|
||
`Sancti/04-29*.txt` at all; no English source translation exists either, so `en` is this
|
||
project's own translation, `draft`), `st-catherine-of-siena` and `st-peter-martyr` (a newly
|
||
found **date-swap wrinkle**: this app's calendar has Catherine on Apr 29 and Peter Martyr on
|
||
Apr 30, the *opposite* of the general Tridentine calendar's own Apr 29/Apr 30 pair — both
|
||
sourced from their real, off-date Tridentine files, the same off-date pattern as St. Paul the
|
||
Hermit in the pre-1955 pass; Peter Martyr in particular has no genuine SanctiM match at either
|
||
date, so also a missed pre-1955-pool candidate). `npm test`/`tsc --noEmit` both pass.
|
||
|
||
**May done (12/13 saints, 1 left unauthored as Common-only)**: `ss-philip-and-james` (Nocturn 3
|
||
has a *full* Gospel pericope, John 14:1-13, not a bare incipit — the first case since
|
||
`chair-of-st-peter-at-rome` in January where `isGospel: true` was actually warranted),
|
||
`st-athanasius`, `ss-alexander-eventius-theodulus-and-juvenal` (missed pre-1955-pool candidate —
|
||
SanctiM's own suffix-matched file just narrows the joint feast, same pattern as
|
||
`ss-vincent-and-anastasius`/`ss-cletus-and-marcellinus`), `st-john-before-the-latin-gate`,
|
||
`st-gregory-nazianzen`, `ss-nereus-achilleus-domitilla-and-pancras`, `st-boniface` (missed
|
||
pre-1955-pool candidate — the date's only SanctiM file is a different, later saint), `st-ubald`
|
||
(same), `st-peter-celestine`, `st-pudentiana` (missed pre-1955-pool candidate — shares its date
|
||
with Peter Celestine, whose SanctiM file the naive per-date check matched instead), `st-urban-i`
|
||
and `st-john-i` (both missed pre-1955-pool candidates via the "plain file is the wrong,
|
||
later saint" wrinkle). Left unauthored: `st-romanus-abbot` — turns out to have a real
|
||
`SanctiM/05-22M.txt` file after all (resolving the earlier "neither file" mystery from the
|
||
pre-1955 pass), but it carries no Lectio content at all, only `vide C5b` (Common of a
|
||
Confessor) — Common-only, not a gap. `npm test`/`tsc --noEmit` both pass.
|
||
|
||
**June done (12/13 saints, 1 left unauthored as no-proper-content)**:
|
||
`ss-marcellinus-peter-and-erasmus`, `ss-primus-and-felicianus`, `st-barnabas`,
|
||
`st-basil-the-great`, `ss-vitus-modestus-and-crescentia`, `ss-mark-and-marcellian` (missed
|
||
pre-1955-pool candidate), `nativity-of-st-john-the-baptist`, `st-william-abbot` (a genuine
|
||
Monastic-only vita exists too, but its content is essentially identical to the Tridentine
|
||
track's own account and only the Tridentine track has an English translation, so sourced from
|
||
there instead), `ss-john-and-paul`, `st-leo-ii` (off-date, like St. Paul the Hermit in the
|
||
pre-1955 pass — the general calendar keeps him on Jul 3, not Jun 28), `ss-peter-and-paul`
|
||
(Duplex I class), `commemoration-of-st-paul`. `vigil-of-st-john-the-baptist` was originally left
|
||
unauthored here as "only a Nocturn 1, no content" — **wrong: `Lectio1-3` turned out to be real
|
||
homiletic content, just filling the Vigil's simpler single-nocturn slot; since authored, see
|
||
"Octave-absorbed and Vigil-only readings" below.** `npm test`/`tsc --noEmit` both pass.
|
||
|
||
**July done (13/18 saints, 5 left unauthored as Common-only)**: `ss-seven-brothers-rufina-and-secunda`,
|
||
`st-pius-i`, `st-john-gualbert`, `st-anacletus`, `st-alexius`, `ss-symphorosa-and-seven-sons`
|
||
(the last four missed pre-1955-pool candidates — see the ninth finding below), `st-bonaventure`
|
||
(a reference-engine translation-misalignment bug — its English section boundaries don't line up
|
||
with the Latin, so `en` here is this project's own translation, `draft`), `st-mary-magdalene`,
|
||
`st-apollinaris`, `st-james-the-greater`, `st-anne`, `st-martha`, `ss-abdon-and-sennen`. Left
|
||
unauthored (Common-only, no proper text at all in the source): `ss-nabor-and-felix`,
|
||
`st-margaret-of-antioch`, `st-liborius`, `st-christopher`, `ss-felix-simplicius-faustinus-and-beatrix`.
|
||
`npm test`/`tsc --noEmit` both pass.
|
||
|
||
**A ninth finding, from July**: the Monastic calendar keeps a full **Octave of St. Benedict**
|
||
(Jul 11-18, since his own feast is Mar 21 but his Monastic-only "Transitus" observance sits in
|
||
July) — every SanctiM file at these dates that isn't a saint's own real match turns out to be
|
||
"Nth day within the Octave of St. Benedict" content, not a genuine candidate mismatch to
|
||
puzzle over. Recognize this pattern (`In Octava S. P. N. Benedicti Abbatis`) and move on
|
||
quickly rather than re-investigating it saint by saint.
|
||
|
||
**August done (13/16 saints, 3 left unauthored as no-proper-content)**: `st-peter-ad-vincula`,
|
||
`ss-machabees` (missed pre-1955-pool candidate), `st-stephen-i` (missed pre-1955-pool, "plain
|
||
file is a different, later saint" trap), `st-dominic`, `ss-cyriacus-largus-and-smaragdus`,
|
||
`st-clare` (missed pre-1955-pool candidate), `st-lawrence` (Duplex I class), `st-bernard`,
|
||
`st-bartholomew`, `st-augustine`, `decollation-of-st-john-baptist`, `st-sabina` (missed
|
||
pre-1955-pool candidate), and `assumption` — the Duplex I class feast of the B.V.M. itself,
|
||
confirming this app's calendar does map the Assumption despite the parent CLAUDE.md's older
|
||
note that fixed BVM/Lord feasts were "not modeled by this app at all yet"; Nocturn 1's own
|
||
scripture (Song of Songs) left to the bible-plan store per this project's usual split, only
|
||
Nocturn 2/3 patristic/Gospel content authored here. Left unauthored (no proper content at all
|
||
in the source, same category as prior Common-only/Vigil exceptions): `ss-nabor-and-felix`-style
|
||
`st-romanus` and `st-hermes` (both Common-only). `vigil-of-st-lawrence` was originally left
|
||
unauthored here as "Nocturn 1 only, no content" — **wrong, same correction as
|
||
`vigil-of-st-john-the-baptist`; since authored, see "Octave-absorbed and Vigil-only readings"
|
||
below.** `npm test`/`tsc --noEmit` both pass.
|
||
|
||
**September done (9/9 saints, full month)**: `nativity-bvm`, `st-hadrian` (missed pre-1955-pool
|
||
candidate), `st-matthew`, `ss-maurice-and-companions` (missed pre-1955-pool), `st-linus`,
|
||
`st-thecla` (missed pre-1955-pool), `ss-cosmas-and-damian`, `st-michael` (Dedication of St.
|
||
Michael the Archangel — its SanctiM file structurally borrows from his own separate May 8
|
||
Apparition file but the actual Lectio content resolves back to this day's own Sancti file),
|
||
`st-jerome`. `npm test`/`tsc --noEmit` both pass.
|
||
|
||
**October done (9/13 saints, 4 left unauthored as Common-only or no-content)**: `st-remigius`,
|
||
`st-francis`, `ss-placid-and-companions`, `st-marcus` (missed pre-1955-pool candidate),
|
||
`st-callistus`, `st-luke`, `st-hilarion`, `ss-chrysanthus-and-daria`, `ss-simon-and-jude`. Left
|
||
unauthored: `st-justina` and `st-gall` (both genuinely Monastic-only — no `Sancti/` file at all
|
||
— but their own `SanctiM` files are themselves Common-only, `vide C6`/`vide C5b`, no proper
|
||
Lectio content to source), `ss-ursula-and-companions` (Common-only, no proper content — matches
|
||
this file's own earlier flag in the "individually-flagged content gaps" section), and
|
||
`vigil-of-all-saints` (re-checked per "Octave-absorbed and Vigil-only readings" below: its
|
||
`Lectio1-3` are genuinely a bare `@Commune/C3:Lectio7-9 in 2 loco` rubric, not real content like
|
||
the other two vigils below turned out to be — correctly still unauthored). `npm test`/`tsc
|
||
--noEmit` both pass.
|
||
|
||
**November done (14/16 saints, 2 left unauthored as Common-only)**: `all-saints`,
|
||
`st-martin-of-tours`, `st-menna` (missed pre-1955-pool), `st-martin-i` (off-date, like St. Paul
|
||
the Hermit — general calendar keeps him Nov 12, this app's own Nov 12 goes to St. Emilian),
|
||
`all-saints-of-the-benedictine-order` (genuinely Monastic-only, and unusually one of the few
|
||
such cases with both rich content *and* an available English translation),
|
||
`dedication-of-the-basilicas-of-ss-peter-and-paul` (missed pre-1955-pool; one sentence of the
|
||
source's own English translation was left untranslated, so `en` for that reading is `draft`),
|
||
`st-pontian` (missed pre-1955-pool), `st-cecilia`, `st-clement`, `st-felicitas` (missed
|
||
pre-1955-pool), `st-chrysogonus` (missed pre-1955-pool), `st-silvester-abbot`,
|
||
`st-peter-of-alexandria` (missed pre-1955-pool), `st-andrew`. Left unauthored (Common-only, no
|
||
proper text at all in the source): `st-emilian`, `st-odo` — both genuinely Monastic-only but
|
||
with no Lectio content in their own SanctiM files, the same wrinkle as `st-justina`/`st-gall`
|
||
in October. `npm test`/`tsc --noEmit` both pass.
|
||
|
||
**December done (13/15 saints, 2 left unauthored as Common-only) — final month, pass
|
||
complete**: `st-bibiana`, `st-nicholas`, `st-ambrose`, `immaculate-conception` (two genuinely
|
||
distinct Nocturn 2 sources kept as separate readings — St. Jerome's sermon, and the Acts of Pius
|
||
IX defining the dogma in 1854), `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` (off-date, missed pre-1955-pool candidate — vu
|
||
places him Dec 15 via Monastic 1617's own same-day redirect, but his real proper content is at
|
||
the general Tridentine calendar's Dec 16; no SanctiM file exists for him at either date). Left
|
||
unauthored (Common-only, no proper text at all in the source): `st-barbara` (correct file
|
||
`Sancti/12-04o.txt`) and `st-melchiades` (correct file `Sancti/12-10o.txt` — this also corrects
|
||
an earlier wrong "neither Sancti nor SanctiM file" claim about him in memory
|
||
`vu-matins-pre1955-saints`, the same "verify by opening the file, not just checking existence"
|
||
lesson as November's St. Romanus Abbot correction). Adding `st-eusebius-of-vercelli`'s content
|
||
surfaced a 3rd Matins lesson on the fixed Advent III Tuesday proof date (`tests/hours/
|
||
matins.test.ts`, Dec 15 falls under his commemoration) — updated that test's expected lesson
|
||
count from 2 to 3, not a regression: exactly the "commemorations get their own reading" design
|
||
working as intended once real content exists to surface. `npm test` (365 tests) and `tsc
|
||
--noEmit` both pass.
|
||
|
||
**Pass complete**: all 12 months (January-December) of the SanctiM-track saints' Matins
|
||
readings are now done — 133 saints authored across the whole year, the rest correctly left
|
||
unauthored as Common-only/no-content cases. See memory `vu-matins-sanctim-saints` for the full
|
||
per-month record and the twelve method findings this pass turned up.
|
||
|
||
### Octave-absorbed and Vigil-only readings — corrected policy, 4 saints authored (2026-08)
|
||
|
||
**Direct instruction, correcting this pass's own earlier judgment call**: "octave-absorbed"
|
||
(the day's Nocturn 2/3 given over to a Nativity/Epiphany homily instead of the saint's own
|
||
vita) and "Vigil, Nocturn 1 only" were both being treated as "no content, correctly left
|
||
unauthored" — wrong. This app pools readings first and figures out how/where to display them
|
||
later (see `hours/matins.ts`'s own `buildReadingPool` header); real, sourceable text at a
|
||
saint's own calendar slot should go into that pool even when it isn't proper to that particular
|
||
saint, rather than being withheld because it came from "a different rubric variant." Four
|
||
saints previously left unauthored for these two reasons are now authored, flagged in each
|
||
file's own header as not-proper-to-the-saint:
|
||
|
||
- `st-telesphorus` (Jan 5) — St. Augustine's Christmas-octave sermon (Nocturn 2) + St. Jerome's
|
||
homily on Matt 2:19-23 (Nocturn 3), neither proper to Telesphorus himself.
|
||
- `st-hyginus` (Jan 11) — St. Fulgentius's Epiphany sermon (Nocturn 2) + St. Ambrose's homily on
|
||
Matt 2:1-12 (Nocturn 3), neither proper to Hyginus himself.
|
||
- `vigil-of-st-john-the-baptist` (Jun 23) — St. Ambrose's homily on Zachary and Elisabeth
|
||
(Luke 1:5-17), authored from `Lectio1-3`, the Vigil's own single 3-lesson nocturn (not the
|
||
usual `Lectio4-9` Nocturn 2/3 slot a full feast uses).
|
||
- `vigil-of-st-lawrence` (Aug 9) — Pope St. Gregory the Great's homily on self-denial
|
||
(Matt 16:24-27), same `Lectio1-3` single-nocturn source.
|
||
|
||
`vigil-of-all-saints` (Oct 31) was re-checked under this same corrected policy and confirmed
|
||
still genuinely contentless — its `Lectio1-3` are a bare `@Commune/C3:Lectio7-9 in 2 loco`
|
||
rubric with no proper text at all, unlike the two Vigils above. Not every "left unauthored"
|
||
case from this pass needs revisiting — only the octave-absorbed and Vigil-only categories were
|
||
affected by this policy correction; genuine Common-only cases (a bare `@Commune` rubric with no
|
||
Lectio text anywhere) are unaffected and remain correctly unauthored.
|
||
|
||
Adding these introduced no new lesson-count regressions in `tests/hours/matins.test.ts` (none
|
||
of its fixed proof dates fall on Jan 5/11 or Jun 23/Aug 9). `npm test` (365 tests) and `tsc
|
||
--noEmit` both pass.
|
||
|
||
### Aug 5 martyrology English gap, and "Our Lady of the Snows" added (2026-08)
|
||
|
||
Two separate fixes, both discovered by re-checking a claim in this file that turned out stale.
|
||
|
||
**Martyrology, Aug 5**: `src/data/martyrology/08-05.yml`'s Latin text opens with "Romæ, in
|
||
Exquíliis, Dedicátio Basílicæ sanctæ Maríæ ad Nives" (the dedication of St. Mary Major, i.e.
|
||
Our Lady of the Snows) — but the file's own English translation silently dropped the
|
||
corresponding sentence, starting straight in on the day's other martyrs instead, even though
|
||
the reference engine's own `English/Martyrologium/08-05.txt` has it ("Upon the 5th day of
|
||
August is kept the dedication of the Church of St. Mary in the Snow upon the Esquiline Hill at
|
||
Rome, in the year 366."). Restored.
|
||
|
||
**"Our Lady of the Snows" (Aug 5) added to the calendar.** `sanctoral-calendar.yml`'s own
|
||
August comment had excluded her entirely as "a fixed BVM feast, same convention as
|
||
Purification/Annunciation" — wrong, on re-reading the actual source: unlike the truly
|
||
unmodeled cases in that same comment (saints permanently subordinate to an unmodeled octave or
|
||
to the fixed Transfiguration), she isn't blocked by any of that, and this app already models
|
||
three other fixed BVM feasts identically (`assumption`, `immaculate-conception`,
|
||
`nativity-bvm`). `Sancti/08-05.txt`'s untagged `[Rank]` block gives her a real Duplex Majus
|
||
rank with real Nocturn 2/3 content (`Lectio4-6`, the miraculous-snowfall founding legend of the
|
||
basilica of St. Mary Major — both Latin and English present, plus a simplex-fallback
|
||
`[Lectio94]` retelling not separately authored). `SanctiM/08-05.txt` confirms she's on the
|
||
Monastic 1617 track too, inheriting `@Sancti/08-05` and reslicing the same Lectio4-6 into its
|
||
own 12-lesson structure — no separate Monastic-only content.
|
||
|
||
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), and `Commune/C11.txt`'s own
|
||
`[Oratio]`/`[Ant 1]` (with full English) supplied both. First pass at this wrongly authored
|
||
that Common text as if it were her own private file via `propers:` — corrected per direct
|
||
feedback: `collect-c11.yml` and `benedictus-antiphon-common-of-the-bvm.yml` are real, reusable
|
||
Common templates now, wired in via `SaintRecord.collectCommon`/`benedictusCommon` exactly like
|
||
any other Common-only saint (e.g. St. Barbara), not a one-off duplicate. `collect-c11`'s own
|
||
text has no `{N}` name placeholder (it addresses "fámulis tuis" generically, not the saint by
|
||
name, unlike every other `collect-c*.yml`) — `collectName` is still required by
|
||
`getDayCollect`'s own lookup, so set to empty arrays, never actually substituted.
|
||
|
||
Live-checked via direct resolution (not a browser query, per
|
||
[[vu-reference-engine-file-layout]]): she wins outright on 2026-08-05 and 2027-08-05, with her
|
||
Benedictus antiphon, day collect, and Matins reading all resolving `verified`. `npm test` (365
|
||
tests) and `tsc --noEmit` both pass.
|
||
|
||
### Four more "not modeled" fixed Lord's/BVM feasts added (2026-08)
|
||
|
||
Per direct instruction ("import the sanctoral content that exists" — this app isn't
|
||
reconstructing any one calendar, and content beyond what the user's own final calendar keeps is
|
||
still worth having so others can build their own): swept every remaining "fixed Lord's/BVM
|
||
feast, not modeled" exclusion in `sanctoral-calendar.yml` the same way Our Lady of the Snows was
|
||
just fixed, rather than treating that as a one-off. All four turned out to have real, complete
|
||
source content too:
|
||
|
||
- **Finding of the Holy Cross** (May 3, Duplex II Classis) — her `[Rule]` is `proprium`: fully
|
||
own text throughout (collect, antiphon, Nocturn 2/3). Now wins outright over
|
||
`ss-alexander-eventius-theodulus-and-juvenal`, exactly matching what that saint's own file
|
||
already documented as the live-verified reality ("loses the day to the Finding of the Holy
|
||
Cross... this isn't just an artifact of the specific year checked") — her file had simply
|
||
never been given a real competitor to lose to until now.
|
||
- **The Transfiguration of Our Lord** (Aug 6, Duplex II Classis) — same `proprium` shape, fully
|
||
own text throughout. Its three subordinate saints (Ss. Xystus/Felicissimus/Agapitus) are
|
||
still excluded — no longer because the Transfiguration itself is unmodeled, but because of
|
||
the same real, separate octave/subordination-rendering gap that already blocks St. Clare et
|
||
al.
|
||
- **The Exaltation of the Holy Cross** (Sep 14, Duplex Majus) — her `[Rule]` is
|
||
`ex Sancti/05-03`: reuses the Finding of the Holy Cross's content wholesale. Collect/antiphon
|
||
reuse via a cross-id `propers` pointer (`propers: finding-of-the-holy-cross`, same pattern as
|
||
`st-michael.yml`'s `apparition-of-st-michael`) rather than duplicate files; the Matins
|
||
nocturn-readings mechanism has no equivalent cross-id lookup (`getNocturnReadings` keys
|
||
strictly off the saint's own id), so `exaltation-of-the-holy-cross.yml` duplicates
|
||
`finding-of-the-holy-cross.yml`'s reading content instead.
|
||
- **Dedication of the Basilica of the Most Holy Savior** (Nov 9, i.e. the Lateran, Duplex II
|
||
Classis) — her `[Rule]` is `ex C8` (Common of the Dedication of a Church, already modeled —
|
||
`collect-c8.yml`/`benedictus-antiphon-common-of-a-dedication.yml` already existed from
|
||
`dedication-of-the-basilicas-of-ss-peter-and-paul.yml`, reused directly here, not duplicated),
|
||
but real unique `Lectio1-6(+6a)` vita content of her own. Her secondary, St. Theodore, is
|
||
still excluded — same real octave/subordination gap as the Transfiguration's secondaries
|
||
above (her own embedded `[Commemoratio 5]` block already carries his real collect too, ready
|
||
to use once that mechanism exists).
|
||
- **The Presentation of the BVM** (Nov 21, Duplex Majus, itself a Monastic-1617-only addition)
|
||
— has a real, unique `[Oratio]` of her own (unlike Our Lady of the Snows), but her
|
||
`[Ant 1]` is a *substituted* Common reference (`@Commune/C10:Ant 2_`, with a
|
||
feast-specific `s/Christo:.*/Christo, allelúja./` truncation applied and computed by hand,
|
||
not shared verbatim by any other saint) — authored as her own file via `propers`, same
|
||
"substitution makes shared-formula text effectively proper" reasoning as
|
||
`st-lucy-collect.yml`.
|
||
|
||
**Correction to Our Lady of the Snows found during this same sweep**: her own file had no
|
||
Lectio7-9 either, and — like the Presentation of the BVM — this means she defers to
|
||
Commune/C11's own Nocturn 3 (a bare Gospel incipit, Luke 11:27-28, plus the Venerable Bede's
|
||
homily). Missed in the first pass (only her Nocturn 2 vita was authored); added now, identical
|
||
content to `presentation-of-the-bvm.yml`'s own Nocturn 3, duplicated for the same
|
||
no-cross-id-lookup reason as Exaltation/Finding of the Holy Cross above.
|
||
|
||
**One real bug caught before committing**: Finding of the Holy Cross's own collect/antiphon
|
||
files were never actually created in the first draft (only Exaltation's cross-id pointer to
|
||
them was written) — both resolved `missing` on live verification. Caught by re-checking every
|
||
new feast end-to-end via direct resolution before committing, not assumed correct from the
|
||
file list alone; fixed immediately, re-verified.
|
||
|
||
**Test updates, not regressions**: three tests had fixed proof dates that happen to collide
|
||
with the newly-added feasts, all updated to reflect the new, correct behavior rather than the
|
||
old absence: `tests/calendar/may-sanctoral.test.ts` (Finding of the Holy Cross now correctly
|
||
outranks its stub competitor instead of winning by the stub's own default), and
|
||
`tests/calendar/day-label.test.ts` (two dates — Nov 9 and 1943-11-21 — now show the new saint's
|
||
name instead of a ferial fallback label, the latter because Duplex Majus already meets this
|
||
app's own pre-existing privileged-Sunday rank threshold in `commemorations.ts`, not new
|
||
behavior). `npm test` (365 tests) and `tsc --noEmit` both pass.
|
||
|
||
### Matins psalmody overrides — mechanism built, one proof (2026-08)
|
||
|
||
Real bug found and fixed: `hours/matins.ts`'s `threeNocturns` gate (Sunday, or a Duplex+
|
||
sanctoral winner) correctly governed *nocturn count*, but the code also unconditionally reused
|
||
the literal Sunday psalmody (`matins-sunday-antiphons.yml`, Ps 20-31) as *content* for every
|
||
three-nocturn day — including a Duplex+ weekday feast, which is wrong. Confirmed live (Monastic
|
||
Tridentinum 1617, `command=prayMatutinum`) against St. Lawrence's own octave closing day
|
||
(2026-08-17, "In Octava S. Laurentii Martyris ~ Duplex"): the real psalmody is Nocturn 1 = Ps
|
||
1,2,4,5,8,10, Nocturn 2 = Ps 14,16,20,23,63,91, each with its own antiphon, plus 3 OT canticles
|
||
in Nocturn 3 (Sirach 14/15, Jeremiah 17, Sirach 31) — a genuinely different scheme from both
|
||
the real-Sunday one and the plain ferial weekday table.
|
||
|
||
Cross-checked against a second Duplex saint (St. Ignatius of Antioch, Common of a
|
||
Martyr-Bishop, no proper content of his own, 2029-02-01): Nocturn 1 identical to Lawrence's
|
||
own; Nocturn 2 nearly identical (only one of six slots differs). So the psalm *numbers* are a
|
||
shared per-Common-category pool, not unique per saint — only the antiphon text is proper to
|
||
the individual saint when authored.
|
||
|
||
Mechanism: `hours/matins-psalmody-overrides.ts` (mirrors `hours/lauds-psalmody-overrides.ts`'s
|
||
shape, but keyed differently — see its own doc comment for why per-category, not per-feast,
|
||
is the right key here) + `data/hours/matins-psalmody-overrides/*.yml`. `matins.ts`'s
|
||
three-nocturn branch now splits on `day.weekday === 'sunday'` first; a non-Sunday three-nocturn
|
||
day looks up an override by the office winner's id, falling back to the plain ferial weekday
|
||
table (redistributed into 3 nocturns instead of 1) when none is authored yet — never blank, but
|
||
honest about not yet having that saint's real proper psalmody.
|
||
|
||
**Proof authored**: St. Lawrence only (`matins-psalmody-overrides/st-lawrence.yml`) — the live-
|
||
verified content above. New scripture chapters added for the canticles:
|
||
`data/scripture/{sir-14,sir-15,jer-17,sir-31}.yml` (sparse, only the cited verses, same
|
||
convention as the pre-existing `sir-36.yml`). `matins.ts`'s canticle shape was generalized from
|
||
one scripture ref per canticle to a list of refs (`canticles: { refs: [...] }[]`), since
|
||
Lawrence's own first canticle cites two Sirach chapters under one heading in the source
|
||
("Eccli 14:22;15:3-4;15:6") — kept as one canticle, not split; `matins-sunday-antiphons.yml`
|
||
migrated to the same shape (each existing single ref wrapped in `refs: [...]`), no content
|
||
change.
|
||
|
||
**Still open** (bulk-content work, same shape as the Lauds override sweep): every other
|
||
Duplex+ Common category (Apostle, Martyr(s), Confessor, Virgin, Doctor, etc. — ~75 saints per
|
||
the earlier Matins design-memory estimate) has no override authored yet and uses the ferial-
|
||
table fallback in the meantime. A real Common-category key (vs. the per-saint key `st-lawrence`
|
||
uses now, since only proper content has been authored so far) is the natural next step once a
|
||
second category's content is authored — see `matins-psalmody-overrides.ts`'s own doc comment.
|
||
|
||
Live-verified end-to-end: `tests/hours/matins.test.ts`'s new "Duplex+ weekday-feast" describe
|
||
block (Lawrence's own psalmody resolving `verified` in both languages across all 12 psalms +
|
||
3 canticles, plus the ferial-fallback case via St. Ignatius). `npm test` (371 tests), `npm run
|
||
build`, and `tsc --noEmit` all pass.
|
||
|
||
### Matins psalmody overrides — category-keyed refactor + second proof, St. Andrew (2026-08)
|
||
|
||
Refactored `hours/matins-psalmody-overrides.ts` per its own doc comment's flagged next step:
|
||
`data/hours/matins-psalmody-overrides/` now splits into `saints/*.yml` (per-saint proper
|
||
antiphon overrides, same shape as before, St. Lawrence migrated in place unchanged) and a new
|
||
`categories/*.yml` store recording each Common category's own generic psalm-number/versicle/
|
||
canticle-ref scheme, live-verified from a saint with *no* proper antiphon of his own (so his
|
||
output is the category default outright). Two categories authored this pass:
|
||
|
||
- **`common-of-a-martyr`**: from St. Ignatius of Antioch (2029-02-01, "vide C2" — entirely
|
||
Common, no proper text). Confirms Lawrence's one real deviation from the shared scheme worth
|
||
remembering: his own Nocturn 2 has Ps 16 where the Common default (and Ignatius) has Ps 64 —
|
||
a genuine proper substitution, not a transcription slip. Also confirmed (reference engine's
|
||
`CommuneM/C2a.txt`) that **Common-of-a-Martyr-Bishop's own Matins psalmody redirects outright
|
||
to Common-of-a-Martyr** — the two share one scheme, not two; checked all three Duplex+
|
||
Common-of-a-Martyr-Bishop saints on the calendar (Ss. Ignatius, Callistus I, Boniface of
|
||
Mainz) and none has a proper antiphon of his own, so this category currently has no saint
|
||
eligible for its own `saints/` entry under the proper-only policy below — a real, not
|
||
accidental, gap.
|
||
- **`common-of-an-apostle`**: from St. Bartholomew (2026-08-24, generic "In omnem terram"
|
||
antiphon). Second live proof authored against it: **St. Andrew** (`saints/st-andrew.yml`,
|
||
2026-11-30, his own day outright that year), whose psalm numbers match the category's
|
||
generic scheme in every one of the 12 Nocturn-1/2 slots — only the antiphon text is his own
|
||
proper narration of his call and martyrdom. New scripture chapters authored for his Nocturn 3
|
||
canticles: `data/scripture/{isa-61,sap-3,sap-10}.yml`.
|
||
|
||
**Category files are reference/documentation content only** — not consulted by
|
||
`getMatinsPsalmodyOverride`, which still does a plain per-saint lookup exactly as before.
|
||
Direct decision from the user (2026-08): a saint without their own `saints/` entry stays on the
|
||
plain ferial-table fallback, never falls through to a category's generic antiphon text, even
|
||
though that text is real and verified — "prefer real text over missing" is deliberately *not*
|
||
extended to this case yet. The category files exist so the next proper-antiphon saint authored
|
||
in an already-proven category doesn't have to re-derive/re-verify the same shared psalm numbers
|
||
and versicles from scratch, and as the reference point for spotting a saint's real deviations
|
||
(like Lawrence's Ps 16) from noise.
|
||
|
||
Live-verified end-to-end: `tests/hours/matins.test.ts`'s new "second Duplex+ weekday-feast
|
||
proof" describe block (Andrew's own psalmody resolving `verified` in both languages across all
|
||
12 psalms + 3 canticles). `npm test` (374 tests), `npm run build`, and `tsc --noEmit` all pass.
|
||
|
||
**Still open**: ~73 remaining Duplex+ saints across the other Common categories (Confessor,
|
||
Confessor-Bishop, Virgin, Doctor, etc. — the largest population, per the earlier estimate) —
|
||
each needs its own category file (one live `prayMatutinum` query against a no-proper-antiphon
|
||
saint) before any of its own saints' proper antiphons can be authored against it.
|
||
|
||
### Matins psalmody overrides — category tier wired in as a real fallback (2026-08)
|
||
|
||
Reversed the "category files are documentation-only" decision above now that two categories
|
||
are trusted: `getMatinsPsalmodyOverride` is now a real three-tier lookup — **proper**
|
||
(`saints/*.yml`) → **Common** (`categories/*.yml`, joined via the winner's own
|
||
`SaintRecord.common` through a new `getSaintRecord` import) → **ferial fallback**
|
||
(`matins.ts`'s own plain weekday table, unchanged, now genuinely the last resort rather than
|
||
the default for every unauthored saint). This is "prefer real text over missing" applied one
|
||
tier further, once the tier itself had proof behind it. `MatinsPsalmodyOverride` and
|
||
`MatinsPsalmodyCategory` now share a `MatinsPsalmodyScheme` base (`nocturn1/2/3`) so the lookup
|
||
can return either without a cast.
|
||
|
||
Category files gained an `aliases` field: `common-of-a-martyr.yml` lists
|
||
`common-of-a-martyr-bishop`, so all three of its Duplex+ saints (Ignatius, Callistus, Boniface —
|
||
confirmed none has proper text of his own) now resolve to real, verified Common psalmody instead
|
||
of the ferial table, with zero new authoring.
|
||
|
||
`tests/hours/matins.test.ts`'s old "falls back to ferial" case (St. Ignatius, 2029-02-01) is
|
||
rewritten as a Common-tier proof (his Nocturn 2 correctly using the Common default's Ps 64, not
|
||
Lawrence's own proper Ps 16); a fresh genuine ferial-fallback proof was added (St. John Bosco,
|
||
Common of a Confessor — no category authored yet for that Common — 2026-01-31, confirmed a
|
||
clean Saturday with no Sunday collision). `npm test` (375 tests), `npm run build`, and
|
||
`tsc --noEmit` all pass.
|
||
|
||
### Matins psalmody overrides — Confessor categories authored, both tiers identical apart from versicles (2026-08)
|
||
|
||
Two more Common categories authored, following the same live-verify-a-saint-with-no-proper-
|
||
antiphon method as Martyr/Apostle:
|
||
|
||
- **`common-of-a-confessor`** (Confessor Not a Bishop, "vide C5"): from St. Francis of Paola
|
||
(2030-04-02, confirmed clean — safely outside Holy Week/Easter octave that year, per his own
|
||
saint-file comment). Nocturn 1 = Ps 1,2,4,5,8,10; Nocturn 2 = Ps 14,20,23,95,96,97; Nocturn 3
|
||
= one antiphon over 3 OT canticles (Sirach 14/15, Jeremiah 17, Sirach 31 — same refs already
|
||
authored for `common-of-a-martyr`, reused as-is).
|
||
- **`common-of-a-confessor-bishop`** ("vide C4"): from St. Basil the Great (2035-06-14, already
|
||
live-verified clean per his own saint-file's note that his rendered Matins is byte-identical
|
||
to this Common despite his own `[Rule]` saying "vide C4a"/Doctor). Live-verified this Common's
|
||
psalm numbers and antiphon text are **word-for-word identical** to `common-of-a-confessor` in
|
||
all three nocturns — the only real difference is the three nocturn-closing versicles, which
|
||
swap plain-confessor imagery for priestly/episcopal imagery (e.g. Nocturn 3's "Tu es sacérdos
|
||
in ætérnum * secúndum órdinem Melchísedech"). Not modeled as an `aliases` redirect (unlike
|
||
Common-of-a-Martyr-Bishop → Common-of-a-Martyr, a genuine zero-difference redirect) because the
|
||
versicles really do differ and the mechanism has no partial-override/diff support — kept as
|
||
its own full duplicate file instead.
|
||
|
||
Checked every Duplex+ saint on the calendar in both Common categories (St. Aloysius Gonzaga,
|
||
St. Anthony of Padua, St. Paschal Baylon, St. John Cantius, St. Bernardine of Siena, St. Joseph
|
||
Calasanctius, St. Raymond Nonnatus, St. Anthony Mary Zaccaria for `common-of-a-confessor`; St.
|
||
Norbert, St. Andrew Corsini for `common-of-a-confessor-bishop`) — none has a proper antiphon of
|
||
his own, so (same as Common-of-a-Martyr-Bishop before it) neither category currently has a
|
||
saint eligible for its own `saints/` entry; a real, not accidental, gap, matching the existing
|
||
policy of only authoring `saints/*.yml` where a genuine proper deviation exists.
|
||
|
||
Two live saints, St. Aloysius Gonzaga (June 21) and St. Norbert (June 6), turned out **not** to
|
||
be on the Monastic Tridentinum 1617 calendar at all despite having a `SanctiM/MM-DD.txt` file —
|
||
confirmed via `Tabulae/Kalendaria/M1617.txt` (no entry) and `Tabulae/Kalendaria/M1930.txt` (only
|
||
Norbert, added there) — both were canonized well after 1617 (Aloysius 1726, though pre-1617 in
|
||
Norbert's case his 1582 canonization still postdates the M1617 calendar's fixed content). Their
|
||
`SanctiM` stub files are shared infrastructure for later Monastic versions (1930/1963), not
|
||
proof the saint was ever actually prayed under M1617 itself — a live `Pofficium.pl` query on
|
||
their own date just falls through to a plain feria. Not a blocker for authoring the Common
|
||
category files themselves (the Common content is shared infrastructure regardless of which
|
||
Monastic version's kalendar wires a given saint into it), but worth remembering next time a
|
||
"vide C" saint's own date doesn't reproduce under `version=Monastic Tridentinum 1617` — check
|
||
`Tabulae/Kalendaria/M1617.txt` before assuming the date/year picked was wrong.
|
||
|
||
`tests/hours/matins.test.ts` gained two new Common-tier proofs (Francis of Paola,
|
||
`common-of-a-confessor`; Basil the Great, `common-of-a-confessor-bishop` — same psalm numbers as
|
||
the other, checked directly). The old ferial-fallback proof (St. John Bosco, `common-of-a-
|
||
confessor`, now superseded) was swapped for Ss. Fabian and Sebastian (`common-of-several-
|
||
martyrs`, still unauthored, 2029-01-20, already live-verified clean). `npm test` (377 tests),
|
||
`tsc --noEmit` pass.
|
||
|
||
### Calendar precedence: a tie against an octave's own elevated closing day now favors the octave (2026-08)
|
||
|
||
Real bug found and fixed, same session as the Matins psalmody fix above, discovered while
|
||
checking why 2026-08-17's Matins/label looked wrong: `calendar/index.ts`'s `applyOctaves` let a
|
||
*tie* between an occurring saint's rank and an active octave's threshold go to the saint
|
||
(`isAtLeast`, `>=`). Live-verified against the reference engine that this is wrong specifically
|
||
for an octave's own elevated *closing* day: St. Hyacinth (plain Duplex, Aug 17) against St.
|
||
Lawrence's own octave closing day (also Duplex, via `closingDayRank`'s default) — the engine's
|
||
own alternate rubric block for that date is titled "Commemoratio S. Hyacinthi Confessoris",
|
||
i.e. Hyacinth is the one merely commemorated, Lawrence's elevated closing day wins outright.
|
||
Matches `collision.ts`'s own existing tie precedent for sanctoral-vs-sanctoral collisions
|
||
("ties favor the incumbent").
|
||
|
||
Fix scoped precisely to closing days, not every octave tie: a tie against an octave's
|
||
*ordinary* (non-closing) threshold still favors the occurring saint, unchanged — confirmed by
|
||
two already-verified counterexamples this app's own tests already carried (St. Thomas of
|
||
Canterbury, Dec 29, against the Christmas Octave's ordinary threshold; St. Nicholas of
|
||
Tolentino, Sep 10, against the Nativity-BVM octave's ordinary threshold) — both would have
|
||
broken under a blanket tie-flip, confirming the closing-day-specific scope is correct.
|
||
`calendar/octaves.ts`'s `ActiveOctave` gained an `isClosingDay` field to make this
|
||
distinguishable.
|
||
|
||
Two related display bugs fixed alongside it: `calendar/day-label.ts`'s `getDayLabel` dropped
|
||
every commemoration (octave or sanctoral) whenever the day's winner was a plain sanctoral
|
||
saint, and dropped every *other* active octave besides the one picked as headline even when an
|
||
octave itself was the winner — both fixed (`otherActiveOctaveNames`), gated the same
|
||
`ordinary-feria`/anchor-day rules the rest of the function already uses so Trinity Sunday's own
|
||
technical overlap with Pentecost's octave still doesn't get named ("nobody calls it that", per
|
||
the function's own pre-existing convention). Rank was also not displayed anywhere in this app's
|
||
UI at all, for any saint — added (`formatRank`), shown after the day's own winner's name, e.g.
|
||
"St. Hyacinth, Confessor (Duplex)".
|
||
|
||
Also closed a real content gap surfaced along the way: `assumption-octave-day-3.yml` (Aug 17,
|
||
the Assumption's own day 3) had never been authored — days 2 and 4-8 existed, day 3 didn't —
|
||
sourced from the reference engine's own recurring Nocturn 2 homily + the octave-wide "Vidi
|
||
speciosam" responsory already used elsewhere in her octave.
|
||
|
||
`tests/calendar/octaves.test.ts` and `tests/calendar/day-label.test.ts` updated to the
|
||
corrected behavior (not weakened) — 2026-08-17 is now a real, live demonstrable date for the
|
||
octave-vs-octave priority pick this app's mechanism already implements, rather than the
|
||
stand-in synthetic-data test that was the only option before.
|
||
|
||
### Bible-plan TSV — bulk conversion done (2026-08)
|
||
|
||
All ~369 non-blank rows of the user's own bible-plan TSV (`~/Downloads/bible-plan.tsv`, 390
|
||
lines including section headers/blanks) are now converted into `data/hours/bible-plan/*.yml`
|
||
— 366 newly generated this pass, plus the 2 proof-date files already authored (`advent-3-tuesday`,
|
||
`post-pentecost-15-sunday`). One row (Ascension Thursday) has no readings in the source and
|
||
was correctly skipped — an honest gap in the user's own plan, not a conversion miss.
|
||
|
||
Conversion was mechanical (script, not hand-authored per file): each TSV "Day" label maps to
|
||
this app's `(temporalId, weekday)` scheme via the same rules `calendar/temporal-id.ts` uses
|
||
(ferias inherit their governing Sunday's id — e.g. "Ash Wednesday" → `quinquagesima`/`wednesday`,
|
||
"Nth Sunday after Trinity" → `post-pentecost-(N+1)` per the existing display-label convention),
|
||
or to a fixed `calendarDate` for the Dec 25 - Jan 13 stretch per the reading-pool redesign
|
||
above. Each `Reading N` column becomes its own `passages` entry; a chapter range like "Isa 36-37"
|
||
expands to one passage per chapter, matching the two proof files' own convention. Every
|
||
generated file sets `status: { la: missing, en: missing }` explicitly (not inferred), same as
|
||
the proof files — correct even where a handful of citations happen to land on already-authored
|
||
`src/data/scripture/*.yml` chapters (e.g. `cant-1`, `cant-8`, `sap-4`), since "verified" should
|
||
mean a human confirmed the reading, not that text merely happened to resolve.
|
||
|
||
A handful of book-abbreviation typos in the source TSV were fixed during conversion (not
|
||
preserved as authored): "Dt 19-2020" → Dt 19-20, "1 Mach 9-10-11" → 1 Mach 9-11, two
|
||
trailing-colon citations ("Mt 7:", "lk 17:") → whole-chapter references. Two day labels were
|
||
garbled by an apparent TSV row-merge artifact ("4th Sunday after Easter of 4th Week after
|
||
Easter", "Sunday after Ascension of Week after Ascension") — both parsed correctly by matching
|
||
just the leading "Nth Sunday after Easter"/"Sunday after Ascension" phrase and ignoring the
|
||
trailing garbage; not fixed in the source file itself since it wasn't touched.
|
||
|
||
One real content question, not a bug: "Sunday after Ascension" 's row has a Gospel reading
|
||
(Jn 17) as its third column — the only Sunday row in the whole TSV with one, seemingly
|
||
contradicting the design note that the user's plan "never assigns a Gospel on a Sunday" (see
|
||
memory `vu-matins-design`). Confirmed with the user (2026-08) as an intended, remembered
|
||
exception, not an authoring slip — keep it as authored.
|
||
`npm test` (359 tests, unchanged — these are new content files, not yet covered by any test
|
||
table) and `npm run build` both pass.
|
||
|
||
Unlike sanctoral/temporal content elsewhere in this project, this plan is the user's own
|
||
reading design (see "Not a reconstruction" in CLAUDE.md), not sourced from a historical
|
||
breviary — there is no reference engine to spot-check these rows against, by design; that's
|
||
not a verification gap to close later.
|
||
|
||
**Not done in this pass:**
|
||
- No verse text was imported — every row still resolves its scripture citation against
|
||
whatever's authored in `src/data/scripture/` (a handful of chapters) and otherwise renders
|
||
`missing`, per the deferred bulk Vulgate/Douay-Rheims import above.
|
||
- Not yet added to any test table (`tests/hours/matins.test.ts` still only covers the two
|
||
original proof dates).
|
||
|
||
- Seasonal variants of the invitatory antiphon, the ferial hymn, and
|
||
Sunday's own psalmody antiphons — all currently one fixed year-round
|
||
text each, even though the source confirms at least the invitatory
|
||
antiphon and Sunday psalmody genuinely do vary by season (e.g. `[Advm0]`
|
||
right after `[Daym0]` in the source).
|
||
- Whether any real supported year reaches a 6th Sunday of Epiphany
|
||
(`post-epiphany-6`) — the user's own plan has no row for it by design;
|
||
confirmed real (some years have it, some don't) but not yet checked
|
||
against which years this app actually needs to support. Resolved
|
||
separately: on such a date, the bible-plan loader returns honestly
|
||
`missing` rather than reusing the 5th week's rows — a deliberate,
|
||
direct-instruction exception to this project's usual "prefer real text
|
||
over missing" stance.
|
||
- The grand closing "Duo Seraphim" responsory before Te Deum (sung after
|
||
the very last lesson, distinct from that lesson's own responsory) isn't
|
||
modeled — the schema only carries one responsory per reading, and that
|
||
closing one doesn't belong to any single reading the way the others do.
|
||
Flagged in `post-pentecost-15.yml`'s own header, not fixed.
|
||
- What "a Gospel reading is its own kind" should mean beyond the `isGospel`
|
||
flag on a plain `lesson` — the user wasn't sure yet what that distinction
|
||
should look like; worth a follow-up once there's a UI to evaluate it
|
||
against.
|
||
|
||
### Vespers ferial Preces + per-feast office overrides — done (2026-08)
|
||
|
||
**Ferial Preces**: `vespers-preces-feriales.yml`, sourced from Tridentine
|
||
1906/1910 (`[Preces feriales Vespera] = @:Preces feriales Laudes:s/129/50/`
|
||
— byte-for-byte the Lauds ferial Preces with Psalm 129 swapped for Psalm
|
||
50, live-verified against the same clean Advent feria 2026-12-17 already
|
||
used for Lauds). The short Sunday/feast form reuses `lauds-short-litany`
|
||
directly rather than a Vespers-named duplicate — confirmed byte-identical
|
||
at both hours by live query (Monastic Tridentinum 1617, both a plain
|
||
Sunday and a plain ferial). The ferial-or-vigil predicate itself
|
||
(`isFerialOrVigil`) was factored out of `hours/lauds.ts` into
|
||
`hours/resolve-common.ts` so both hours' `-preces` cases share one
|
||
definition instead of two copies. See `hours/vespers.ts`'s `vespers-preces`
|
||
case and `hours/types.ts`'s doc comment.
|
||
|
||
**Per-feast office overrides**: same eligibility rule as Lauds'
|
||
`getPsalmodyOverrideFor` (a duplex-majus+ saint, or a named temporal id in
|
||
`ALWAYS_OVERRIDE_TEMPORAL_IDS`), factored out to `resolve-common.ts`'s
|
||
`getOfficeOverrideId` since Vespers has no psalmody-override table of its
|
||
own to piggyback eligibility on the way Lauds does. **Authored — all 30
|
||
ids with an existing Lauds office-bundle override, full 30/30**, one live
|
||
`command=prayVespera` query each (Monastic Tridentinum 1617, except
|
||
`christ-the-king`: Monastic Divino 1930, same as its Lauds counterpart),
|
||
same clean per-saint dates already established for the Lauds sweep.
|
||
Extraction automated via a purpose-built parser
|
||
(`scratchpad/fetch_vespers.py`, not committed) after the first hand-check
|
||
(St. Lawrence) confirmed the page structure; validated by diffing every
|
||
extracted capitulum against its already-authored `lauds-capitulum-<id>`
|
||
counterpart before trusting the rest, plus an automated missing-field/
|
||
leaked-response-marker sweep across all 30 outputs, plus a runtime smoke
|
||
test resolving each id's actual chapter/responsory/hymn/versicle bundle
|
||
through `resolveOrdo`, not just checking the YAML files exist.
|
||
|
||
**Real finding, not assumed**: the capitulum is byte-identical between
|
||
Lauds and Vespers for every one of the 30 ids checked — a genuine
|
||
Monastic-rite fact (the day's reading doesn't change between the two
|
||
hours, only the responsory/hymn/versicle do). So no `vespers-capitulum-
|
||
<id>.yml` files exist at all; `hours/vespers.ts`'s
|
||
`vespersCapitulumForOverride` falls through to the already-authored
|
||
`lauds-capitulum-<id>` directly instead of duplicating identical content.
|
||
|
||
St. Scholastica needed the same hand-translation treatment her Lauds/
|
||
Terce/Sext/None content did: no English exists in the reference engine's
|
||
data for her Vespers responsory, hymn ("Te beáta sponsa Christi"), or
|
||
versicle — translated here (`status: draft` on the English), distinct text
|
||
from her Lauds hymn since Vespers' proper content doesn't overlap with the
|
||
minor hours' shared pool the way a Common-category saint's does. Also two
|
||
one-line English gaps unrelated to her (`immaculate-conception`,
|
||
`nativity-bvm`'s shared Ave Maria responsory — the reference engine left
|
||
"Benedícta tu in muliéribus..." untranslated for both): filled with the
|
||
standard Douay-Rheims/Ave Maria wording, not a free translation.
|
||
|
||
**Known, not modeled**: 5 of the 30 hymns (`ss-philip-and-james`,
|
||
`st-john-apostle`, `st-john-before-the-latin-gate`, `st-mark`,
|
||
`st-stephen-protomartyr`) carry a `{Doxology: PaschT}`/`{Doxology: NatT}`
|
||
tag in the source, meaning their closing doxology stanza is itself
|
||
seasonally variable — what's stored is whichever form rendered live on
|
||
each one's already-chosen clean query date, not a season-aware
|
||
substitution. Same class of gap as the Paschaltide alleluia suffix
|
||
elsewhere in this file, flagged not fixed.
|
||
- **Easter's own octave** — explicitly tabled ("easter needs a whole
|
||
different discussion").
|
||
|
||
### The "which Sunday governs this date" temporal-id algorithm — done (2026-08)
|
||
|
||
Overflow years (Easter early enough that fewer than 6 Sundays after Epiphany occur before
|
||
Septuagesima cuts in) previously just clamped every excess post-Pentecost Sunday to
|
||
`post-pentecost-24` — wrong for every week between XXIII and the real last Sunday of the
|
||
year, only accidentally right for that final Sunday itself.
|
||
|
||
`calendar/temporal-id.ts`'s post-Pentecost branch is now ported directly from the reference
|
||
engine's own `getweek()` (`DivinumOfficium/Date.pm`) rather than re-derived by hand: once the
|
||
governing Sunday's Pentecost week reaches XXIII, a `wdist` (weeks until Advent) calculation
|
||
decides the id — the fixed final Sunday (`wdist < 2`) always resolves to `post-pentecost-24`
|
||
regardless of the true elapsed count, `post-pentecost-23` covers its own normal week, and
|
||
everything else resumes as `post-epiphany-${8 - wdist}` — the skipped post-Epiphany Sundays,
|
||
in their own ascending order, immediately before the fixed final Sunday. No separate tracking
|
||
of "how many post-Epiphany Sundays occurred" is needed — it falls out of the Advent-distance
|
||
arithmetic alone, confirmed against the reference engine's source rather than assumed. Trinity
|
||
Sunday = `post-pentecost-01` (this app's existing convention) also confirmed correct against
|
||
the same source (`$n=1` at `easter+56`, matching `TemporaM/Pent01-0.txt`'s own Trinity-Sunday
|
||
content).
|
||
|
||
Separately, `calendar/index.ts` gained `applyEpiphany6Commemoration`, an `apply*`-pass
|
||
(same shape as `applyChristTheKing`) that commemorates `post-epiphany-6` on the Saturday
|
||
immediately before Septuagesima — but only in years where Epiphany V (not IV, III, ...) was
|
||
the last post-Epiphany Sunday to actually occur, per direct clarification (a narrower rule
|
||
than "any skipped Sunday gets a Saturday commemoration" — only VI specifically, and only in
|
||
that one adjacency case). No new `Commemoration` variant was needed: the existing
|
||
`{ kind: 'temporal'; id }` shape already round-trips through `getDayCollects`'s
|
||
`${id}-collect` lookup for free, so this is purely additive to `commemorations`, coexisting
|
||
with whatever else that Saturday already resolved to (consistent with this app's generous,
|
||
non-single-winner commemoration design elsewhere).
|
||
|
||
Verified live against real years, not just unit-tested: 2026 (`post-epiphany-5` is the last
|
||
Sunday reached, so `2026-11-14`/`-21` resume as Epiphany V/VI before `2026-11-28`'s fixed
|
||
`post-pentecost-24`), and 1943/1962 (Easter late enough that all 6 Epiphany Sundays occur, so
|
||
no resumption and no Saturday commemoration ever fires). New test file
|
||
`tests/calendar/temporal-id.test.ts` covers both the overflow resumption and the Saturday
|
||
commemoration's on/off cases (2026, 2022, 2025, 1943). `npm test` (363 tests, 4 new) and
|
||
`npm run build` both pass.
|
||
|
||
**Follow-up done same day:** `calendar/day-label.ts`'s on-screen display now also tracks the
|
||
resumed-Sunday content, via a `trinitytideOverrideLabel` check ahead of the ordinary
|
||
Trinity-counted branch — it calls `resolveTemporalId` directly (the same mechanism the content
|
||
layer trusts) rather than re-deriving the wdist arithmetic a second time, so the label can
|
||
never disagree with what's actually rendered underneath. A resumed Sunday/weekday now reads
|
||
"The 5th Sunday after Epiphany" / "Monday in the 5th week after Epiphany" instead of continuing
|
||
the Trinity count past its real bound.
|
||
|
||
**Correction, same pass**: the first cut of this left the fixed final Sunday of the year
|
||
(`post-pentecost-24`) on the old raw "weeks since Trinity" arithmetic, on the assumption that
|
||
was an unrelated, already-correct convention — wrong. Scanning every year 1900-2100 showed
|
||
*every single year's* last Sunday before Advent resolves to `post-pentecost-24`, and the raw
|
||
elapsed-week count for that Sunday varies year to year (22nd, 23rd, 25th, ... depending on
|
||
Easter's date) even though the content is always the same fixed formulary. Since this app's
|
||
Trinity-counted display is one week off from the Pentecost-counted id (Trinity Sunday itself =
|
||
`post-pentecost-01` = the display's own uncounted anchor), the correct display ordinal for
|
||
that fixed Sunday is XXIV − 1 = **23, always** — not derived from elapsed weeks at all.
|
||
`trinitytideOverrideLabel` now handles this as its own branch (`FIXED_LAST_SUNDAY_ORDINAL`),
|
||
same "ask `resolveTemporalId`, don't re-derive" principle as the resumed-Epiphany branch.
|
||
Live-verified across 1943/1962/2015/2026/2033 — all five now read "The 23rd Sunday after
|
||
Trinity" for their own last Sunday, where before the fix they'd shown four different numbers.
|
||
Tests in `tests/calendar/day-label.test.ts` updated accordingly. `npm test` (365 tests) and
|
||
`tsc --noEmit` both pass.
|
||
|
||
### Bilingual, merged date/day-label header — done (2026-08-18)
|
||
|
||
The civil date/weekday (`day-nav`'s date span) and the "day being celebrated" text
|
||
(`getDayLabel`, previously repeated inside every hour's own view) are now merged into one
|
||
header block in `day-nav.ts`, and both are bilingual. `getDayLabel()` returns
|
||
`Partial<Record<string,string>>` (`{en, la}`) instead of a plain English string, matching
|
||
`hours/types.ts`'s existing bilingual convention; `formatDateLong()` (`src/ui/format.ts`) is
|
||
bilingual too, since `Intl` has no Latin locale data (`MONTH_LABELS_LA` authored directly).
|
||
Latin weekday names are spelled out per direct instruction — "Feria Tertia", not "Feria III"
|
||
— `Dominica`/`Feria Secunda`...`Sexta`/`Sabbato`. Advent and Lent specifically drop the
|
||
redundant leading weekday word from `temporalLabel()` now that the header always shows the
|
||
weekday on its own ("Tuesday in the 2nd week of Advent" → "In the 2nd week of Advent") — every
|
||
other season (Epiphanytide, Eastertide, Trinitytide, the plain fallback) keeps its weekday
|
||
prefix, a deliberate narrower-scope decision, not an oversight.
|
||
|
||
**Real content gap, left open on purpose**: no Latin form exists anywhere for saint names
|
||
(`SanctoralIdentity.name`), octave names, or named-temporal-feast names — only rank labels,
|
||
weekday names, month names, and season/ordinal phrasing have authored Latin. Every saint/
|
||
octave/feast name currently falls back to its English string as its own `la` value too (same
|
||
"surface real text over missing" convention used elsewhere), so Latin mode never shows a blank
|
||
where a proper name belongs — but it also means Latin mode still reads mostly-English for any
|
||
day with a real winner. Authoring actual Latin names per saint/octave/feast is separate future
|
||
content work (hundreds of records), not started here.
|
||
|
||
`tests/calendar/day-label.test.ts` rewritten for the `{en, la}` shape (all `.en` values
|
||
unchanged except the Advent/Lent weekday-drop cases), plus new representative `.la`
|
||
assertions (weekday, rank, season-ordinal genitive construction, English-fallback-for-a-
|
||
saint-name). `tests/hours/compline.test.ts` and `tests/ui/shell.test.ts` updated too. `npm
|
||
test` (383 tests) and `tsc --noEmit` both pass; manually verified in the browser (English,
|
||
Latin, and both-language modes) against an Advent date with an active octave.
|
||
|
||
### Ferial weekday Matins nocturn antiphons + versicle — done (2026-08-21)
|
||
|
||
Real gap found (user flagged it directly): the plain ferial weekday Matins nocturn
|
||
(`hours/matins.ts`'s single-nocturn branch, no Sunday/Duplex+ override) rendered bare psalms
|
||
with no antiphon at all — not a documented design choice like `ferialPsalmodyThreeNocturns`'s
|
||
antiphon-less fallback, just an oversight from before antiphons existed anywhere in this hour.
|
||
|
||
Source: Divinum Officium's Monastic-labeled Matins psalter, `Latin/Psalterium/Psalmi/Psalmi
|
||
matutinum.txt`'s `[Daym1_]`-`[Daym6_]` sections (the Latin file's own unsuffixed `[Daym1]` etc.
|
||
just redirects to these via `@:` references), English parallel at the *unsuffixed*
|
||
`[Daym1]`-`[Daym6]` keys in the `English/` tree (no indirection there). Real wrinkle:
|
||
`psalter-distribution.yml`'s own `matins:` table isn't the historical per-weekday psalm set —
|
||
its own header documents a deliberate "editorial redistribution" (54 non-Sunday psalms
|
||
reclassified must-stay/could-move by length, then resliced into 6 fresh groups of 9 in
|
||
continuous psalm order), which breaks several of the source's own antiphon-sharing pairs across
|
||
weekday boundaries (e.g. Ps 43/44, originally Monday's own closing pair, now open Tuesday
|
||
instead). Resolved by keeping vu's existing 9-psalm weekday lists untouched and regrouping:
|
||
every psalm keeps its own original antiphon text, consecutive same-antiphon psalms stay grouped
|
||
whenever their pair survived the reslice intact, and a psalm whose original partner moved
|
||
elsewhere now stands alone under its own antiphon. Each weekday's single versicle is taken from
|
||
whichever source day contributes the majority of that weekday's 9 psalms, placed at the same
|
||
relative position it held there (verified group-by-group against both parallel source files —
|
||
see each weekday's own comment in the new data file for the exact reasoning).
|
||
|
||
New `data/hours/matins-ferial-antiphons.yml` (6 weekdays, 5-6 antiphon groups each, exactly one
|
||
group per weekday carrying a versicle) + `hours/matins.ts`'s new `ferialAntiphonedNocturn`,
|
||
replacing the old bare `ferialPsalmody` call in the plain-ferial branch (`ferialPsalmody` itself
|
||
stays, still used by `ferialPsalmodyThreeNocturns`'s intentionally antiphon-less fallback).
|
||
Versicle rendered as one combined V./R. block (`"V. ...\nR. ..."`) via a new shared
|
||
`versicleText` helper.
|
||
|
||
**Follow-up, same day: fixed the pre-existing Sunday versicle bug too.** `sundayPsalmNocturn`/
|
||
`sundayCanticleNocturn` (used by both Sunday Matins and every Duplex+ weekday feast's
|
||
`matins-psalmody-overrides` content) only ever pushed `group.versicle.v` — the V. line — never
|
||
the R. line, so every Sunday/override nocturn versicle has been silently missing its responsory
|
||
since the Sunday psalmody was first authored. Both functions now go through the same
|
||
`versicleText` helper the ferial nocturn code above uses. `tests/hours/matins.test.ts` gained a
|
||
regression test asserting all 3 Sunday nocturn versicles render both lines.
|
||
|
||
`tests/hours/matins.test.ts` gained a new assertion (on the existing `FERIAL_DATE` proof,
|
||
2026-12-01, Tuesday of Advent I) confirming every nocturn psalm gets its own antiphon,
|
||
correctly grouped, and exactly one versicle appears at the right point. Also fixed 3 stale
|
||
Triduum tests that asserted "no versicle part anywhere in the whole ordo" — true only because
|
||
ferial nocturns had no versicles at all before this change; now checks specifically that the
|
||
dropped *opening* versicle (and Ps 3, which it precedes) is gone, via `ordo.parts[0]` being the
|
||
nocturn's own first psalm, rather than asserting zero versicles exist. `npm test` (421 tests,
|
||
including the Sunday-versicle regression test above) and `tsc --noEmit` both pass.
|
||
|
||
### Immaculate Heart of Mary relocated off Aug 22 (2026-08-22)
|
||
|
||
User-flagged, same day: Aug 22 (the Assumption's octave-closing day) was being won outright by
|
||
Immaculate Heart of Mary (Duplex II classis, fixed there since Pius XII's 1944 decree),
|
||
permanently demoting the octave day to a commemoration — a real collision between this
|
||
project's two blended calendar tracks (Divino Afflatu fixes IHM there; the older Tridentine
|
||
track has no IHM at all, just the octave day). Per this project's "generous, not
|
||
reconstructionist" design, the fix is relocation, not picking a permanent winner.
|
||
|
||
Researched the real history rather than guessing: Immaculate Heart of Mary was actually
|
||
celebrated on the **Saturday after the Feast of the Sacred Heart** from 1914 until the 1944
|
||
fixed-date decree, and that's also the date the 1969 reform returned to — continuity on both
|
||
sides of the Aug-22 interlude, unlike an initially-considered but unverifiable "Saturday after
|
||
the *octave* of Sacred Heart" guess (no such rule was ever real, and Sacred Heart itself has no
|
||
octave modeled in this codebase — see `data/calendar/easter-offsets.yml`, where it's only a
|
||
season-label entry, `68: sacred-heart`).
|
||
|
||
Moved: `data/calendar/saints/immaculate-heart-of-mary.yml` deleted, its `sanctoral-calendar.yml`
|
||
`"08-22"` entry removed. New `data/calendar/temporal-feasts/immaculate-heart-of-mary.yml`
|
||
(id/name/rank only, no octave). New `calendar/index.ts` function `applyImmaculateHeart` —
|
||
computes Easter+69 (Sacred Heart, Easter+68, is always a Friday; this is the Saturday right
|
||
after), overriding the day's winner there via the same rank-compared, commemorate-the-loser
|
||
pattern `applyChristTheKing`/`applyMarianSaturday` already use, run last in `resolveDay`'s chain
|
||
so a specific named feast always supersedes the generic "Our Lady's Saturday" default rather
|
||
than losing to whichever ran first. Added `immaculate-heart-of-mary` to `hours/resolve-
|
||
common.ts`'s `ALWAYS_OVERRIDE_TEMPORAL_IDS` so its own chapter/responsory/hymn/versicle/
|
||
antiphon overrides are actually eligible (a temporal-kind winner is otherwise ignored by
|
||
`getOfficeOverrideId`).
|
||
|
||
**Superseded the same day** — see "Generic Easter-offset movable-feast mechanism" below:
|
||
`applyImmaculateHeart` itself no longer exists; the same behavior now runs through
|
||
`calendar/movable-feasts.ts`'s generic `applyMovableFeasts`, with IHM as its first data-driven
|
||
entry (`easterOffset: 69` on its own temporal-feasts YAML record).
|
||
|
||
Propers content moved stores, not just files — temporal feasts resolve via a physically
|
||
different store than sanctoral saints (`getTemporalProper`/`data/propers/temporal/*.yml`, keyed
|
||
`${id}-${suffix}`, vs. sanctoral's `common`/`propers`-field indirection into `data/propers/
|
||
common/*.yml`). Collect moved as-is (suffix already matched); the single shared antiphon file
|
||
was split into `-benedictus-antiphon`/`-magnificat-antiphon` (temporal's two-suffix convention,
|
||
vs. sanctoral's one file reused for both) — same duplicated text, no new content authored.
|
||
Nocturn readings needed no file change, but did surface a real bug in `hours/matins.ts`'s
|
||
`nocturnReadingIds`: it only ever added `day.winner.id` when `day.winner.kind === 'sanctoral'`,
|
||
so a named *temporal* override (IHM now, or `marian-saturday`/`christ-the-king` already, neither
|
||
of which happened to have nocturn-readings content authored yet to expose this) would never
|
||
have picked up its own reading file — fixed by adding `day.winner.id` unconditionally (harmless
|
||
dedup via the existing `Set` on an ordinary day, where it already equals the plain temporal id).
|
||
|
||
`tests/calendar/immaculate-heart-of-mary.test.ts` (new) proves the relocation end-to-end across
|
||
two years' Easter dates, and `tests/calendar/august-sanctoral.test.ts`'s existing Aug 22 test
|
||
was updated to match (the day now reverts to the plain Assumption octave, same shape as St.
|
||
Lawrence's own Aug 12 case). `npm test` (429 tests) and `tsc --noEmit` both pass.
|
||
|
||
### Assumption-octave Matins hymn fixed (2026-08-22)
|
||
|
||
Separately, the "no antiphons and a weird hymn today" complaint that prompted the investigation
|
||
above turned out to be a distinct, narrower bug: `resolveMatinsHymn`'s own doc comment
|
||
already claimed the Assumption's octave "keeps the feast's own proper hymn all week," but the
|
||
code never implemented that — it only fired when the day's own winner literally *was*
|
||
`assumption` (Aug 15 only). Every other octave day (16, 17, 19, 20, 21 — St. Joachim, St.
|
||
Hyacinth, St. John Eudes, St. Bernard, St. Jane Frances de Chantal, none of whom has a Matins
|
||
hymn of their own authored) fell straight to the plain ferial hymn. Fixed by adding a real
|
||
octave-fallback tier (`resolveActiveOctave(day.date)` → `matins-hymn-${octave.id}`) between the
|
||
existing per-feast override and the seasonal tier. Confirmed against the reference engine that
|
||
Monastic 1617 itself doesn't do this either (no octave day there has its own `[Hymnus
|
||
Matutinum]` override) — this is a deliberate departure, not a restoration, consistent with how
|
||
this project already treats octave readings/commemorations more generously than any one source
|
||
track. Aug 18 (no sanctoral winner) and Aug 22 (once IHM moved) needed no change — both already
|
||
reached `matins-hymn-assumption` via the pre-existing `resolveOfficeWinner` octave-substitution
|
||
path, which only fires when the day's own winner isn't sanctoral.
|
||
|
||
**Not fixed, logged instead** (user decision): the deeper reason Matins had *zero* nocturn
|
||
antiphons at all on Aug 22 — no `matins-psalmody-overrides` entry exists for `assumption` or
|
||
`immaculate-heart-of-mary` (or, generally, any Duplex-2-classis+ feast lacking one), so Matins
|
||
falls all the way to the bare ferial psalm table, which by design carries no antiphons — is a
|
||
broader content gap than today's date, tracked in the "Matins psalmody overrides" bullet above
|
||
rather than authored now. Also newly true because of this change: `hours/matins.ts`'s psalmody
|
||
override lookup is hard-gated to `winner.kind === 'sanctoral'`, so even if IHM's psalmody were
|
||
authored later, it wouldn't apply as a temporal-kind winner without that gate being loosened
|
||
too — noted for whoever picks this up.
|
||
|
||
New tests in `tests/hours/matins.test.ts` cover the fallback across Aug 16-22, including the
|
||
Aug 17 edge case (correctly still ferial, since that day is actually governed by St. Lawrence's
|
||
own octave, not the Assumption's). `npm test` (430 tests) and `tsc --noEmit` both pass.
|
||
|
||
### Generic Easter-offset movable-feast mechanism (2026-08-22)
|
||
|
||
User feedback, same day: a bespoke `calendar/index.ts` function per non-Sunday movable feast
|
||
(`applyImmaculateHeart`, following the `applyMarianSaturday`/`applyChristTheKing` precedent) was
|
||
flagged as fragile going forward — real plans exist for several more feasts that are all
|
||
"weekday N days from Easter Sunday": Ember/Rogation days, more Sacred-Heart-family Marian
|
||
devotions, St. Joseph's own pre-1955 Eastertide feast, and the Lenten Friday Passion devotions.
|
||
Writing a new function per feast doesn't scale and invites the same subtle ordering bugs
|
||
(`applyImmaculateHeart` had to be sequenced *after* `applyMarianSaturday` specifically, or a
|
||
Saturday-anchored feast would have been silently clobbered by the generic "Our Lady's Saturday"
|
||
default — easy to get wrong once there are several of these).
|
||
|
||
Replaced with a single generic resolver, `calendar/movable-feasts.ts`'s `applyMovableFeasts`,
|
||
run in `applyImmaculateHeart`'s old spot in `resolveDay`'s chain. No new data table: it scans
|
||
`calendar/temporal-feasts.ts`'s existing per-feast YAML records (`data/calendar/
|
||
temporal-feasts/*.yml`, already carrying `id`/`name`/`rank`/`octave`) for a new optional
|
||
`easterOffset` field, added to the `TemporalFeastRecord` type. Immaculate Heart of Mary's own
|
||
record gained `easterOffset: 69`; adding the next feast is a new YAML file with `rank` and
|
||
`easterOffset` set, not a new TypeScript function. Same rank-compared, commemorate-the-loser
|
||
semantics as before, applied generically instead of duplicated per feast.
|
||
|
||
Deliberately doesn't try to unify everything: `christ-the-king` (last Sunday of October),
|
||
`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) are structurally different rules,
|
||
not Easter-offset ones — they stay as their own small functions in `calendar/index.ts` rather
|
||
than being force-fit into this table for the sake of a single mechanism. Ember days for the
|
||
September and Advent quarters are anchored to a *fixed civil date's* nearest Sunday, not Easter,
|
||
so they'll need a second anchor kind on `TemporalFeastRecord` when they're actually authored —
|
||
not modeled yet, deliberately: the other two Ember quarters (Lent I, Pentecost) and Rogation
|
||
days, on the other hand, *are* plain Easter offsets and are already covered by this mechanism
|
||
today.
|
||
|
||
No behavior change — `tests/calendar/immaculate-heart-of-mary.test.ts` (unchanged) still passes
|
||
end-to-end through the new generic path. `npm test` (430 tests) and `tsc --noEmit` both pass.
|
||
|
||
### September + Advent Ember days — new mechanism + real content authored (2026-08-22)
|
||
|
||
Follow-on from the movable-feasts generalization above: researched whether Ember days belonged
|
||
in that same table (they don't — see below) and, once that was settled, built their actual
|
||
mechanism and authored real Matins content for all 6 days as the first two proof cases.
|
||
|
||
**Not a `movable-feasts.ts` entry.** Live-verified against the reference engine (Monastic
|
||
Tridentinum 1617): September Ember Wednesday 2026 was won outright by Ss. Cornelius & Cyprian
|
||
(Semiduplex) with the Ember feria demoted to `Commemoratio ad Laudes tantum` — i.e. Ember days
|
||
are **privileged ferias**, not named feasts contesting the day by `FeastClass` rank the way
|
||
Christ the King/Immaculate Heart of Mary do. Two separate, smaller pieces of new mechanism
|
||
instead:
|
||
|
||
- **Precedence**: `calendar/temporal.ts`'s new `septemberEmberDayOffset` (the Sunday nearest
|
||
Sept 14, i.e. the Exaltation of the Holy Cross, +3/+5/+6 for Wed/Fri/Sat — same "nearest"
|
||
arithmetic `adventStart` already used for "Sunday nearest Nov 30," factored out into a shared
|
||
`nearestSunday` helper) feeds a new check in `resolveTemporalCategory`, giving those 3 dates
|
||
`privileged-feria-minor` — live-verified as the correct tier. Advent Ember days needed **no**
|
||
precedence change at all: Advent's own season default is already `privileged-feria-minor`,
|
||
and no live evidence turned up that Advent Ember days need a stronger tier (`adventEmberDayOffset`
|
||
only exists for the content-id side below, not this one). Note this settles the "not
|
||
worth guessing at" gap flagged in `temporal-categories.yml`'s own header comment, added back
|
||
when this was still unmodeled.
|
||
- **Content identity**: new `calendar/ember-days.ts`'s `applyEmberDay`, run right after
|
||
`applyOctaves` in `resolveDay`'s chain (same octave-guard `applyMarianSaturday` uses — the
|
||
Nativity of the BVM's own octave, Sep 8-15, can genuinely overlap a late-anchored September
|
||
Ember week in some years). When the feria itself is what's winning (no saint strong enough to
|
||
clear the `privileged-feria-minor` threshold), its `winner.id` is relabeled to the Ember day's
|
||
own id (`ember-september-wednesday`, etc.) so its real proper collect/readings are found
|
||
instead of inheriting whatever Sunday governs that week; when a saint wins outright instead,
|
||
the Ember day is pushed as a `{kind: 'temporal', ...}` commemoration alongside it (this app
|
||
doesn't model the "Laudes only" hour-scoping nuance the live rubric shows — same
|
||
simplification level as every other commemoration here).
|
||
|
||
**Real bug found and fixed along the way**: `hours/matins.ts`'s `nocturnReadingIds` only ever
|
||
pulled in a *sanctoral* commemoration's own id, never a *temporal* one's — so a commemorated
|
||
Ember day (the common case: most Ember days lose the precedence contest to whatever saint
|
||
occurs) would never have surfaced its own nocturn readings at all. Fixed by extending that
|
||
check to `c.kind === 'sanctoral' || c.kind === 'temporal'`. Same root cause, same fix shape, as
|
||
the day.winner.id gap the Immaculate Heart of Mary relocation surfaced above — this one just
|
||
hid in the commemorations loop instead.
|
||
|
||
**Content authored**: all 6 days' real collect + 3 Matins readings (Gospel-citation-plus-homily
|
||
pattern, bare Gospel incipits skipped per this store's own established convention — see
|
||
st-lawrence.yml/st-andrew.yml — only the homily portion transcribed), read directly from
|
||
`Tempora/093-{3,5,6}.txt` (September) and `Tempora/Adv3-{3,5,6}.txt` (Advent, whose Lectio1-3
|
||
are themselves a cross-reference to the Annunciation's own Common, `Sancti/03-25.txt` — followed
|
||
and transcribed from there). Kept as 3 separate readings per day rather than combining into one
|
||
(this store's usual default when several lessons share one continuous source) because each
|
||
carries its own genuinely distinct, proper responsory — collapsing them would have discarded
|
||
real, valuable content for no good reason, unlike the cases that default was written for.
|
||
September Wednesday/Saturday use the Monastic-track's own override responsories (drawn from
|
||
`TemporaM/093-0`'s Tobias-sermon texts) in place of the secular ones, per this project's
|
||
Monastic-1617-first structure preference — Friday and both Advent Ember days are identical in
|
||
both tracks (no `TemporaM/Adv3-{3,5,6}` override files exist at all).
|
||
|
||
**Not authored, logged instead**: the Ember-day-specific `[Ant 2]`/`[Ant 3]` Lauds antiphons
|
||
found during research — scoped out deliberately, same "collect + Matins first, other hours
|
||
later" incremental pattern used throughout this project's sanctoral sweep. Also not modeled:
|
||
the Monastic track's fuller 12-lesson/12-responsory Matins structure (a 12th responsory exists
|
||
in the source for all 3 September days) — this app's reading-pool mechanism doesn't preserve a
|
||
literal historical lesson count for any day, sanctoral or temporal alike, so this isn't a new
|
||
gap specific to Ember days.
|
||
|
||
New `tests/calendar/ember-days.test.ts` covers the precedence contest (September, live-verified
|
||
saint-wins-Ember-commemorated cases) and content resolution (Advent, wins-outright cases; real
|
||
verified Matins content on all 6 days) end-to-end. `npm test` (435 tests) and `tsc --noEmit`
|
||
both pass.
|
||
|
||
### Matins nocturn-reading temporal/month-week suppression bug, hymn Common-tier + real invitatory mechanism, antiphon-doubling dedup (2026-08-24)
|
||
|
||
**Bug fixed**: `nocturnReadingIds` (`hours/matins.ts`) unconditionally pooled the plain
|
||
temporal-cycle reading id and the calendar-month/week id into every day's nocturn reading
|
||
pool, even on a day where a real feast wins outright with zero commemorations
|
||
(`calendar/commemorations.ts`'s `decideOccurrence`, `ordinary-feria` branch — correctly
|
||
suppressing 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
|
||
(`post-pentecost-13.yml`) and month-week (`month-week-084.yml`) readings wrongly filled his
|
||
Nocturn 3. Fixed by gating that pooling on whether the day's own occurrence decision actually
|
||
retained the temporal identity (`day.winner.kind === 'temporal'`, or a `kind: 'temporal'` entry
|
||
in `day.commemorations`) — mirrors `getDayCollects`'s own long-standing pattern for the same
|
||
question. New tests in `tests/hours/matins.test.ts` cover both the regression (Bartholomew) and
|
||
the non-regression case (a plain ferial weekday with no sanctoral winner, 2026-09-04).
|
||
|
||
**Matins hymn + invitatory antiphon — new Common-of-Saints tier, real invitatory mechanism**:
|
||
also fixed while investigating the same report. `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 —
|
||
no per-octave invitatory override exists anywhere in the reference source) before the
|
||
pre-existing doubling step is applied. Content authored this pass: Common-of-an-Apostle only,
|
||
both hymn (`matins-hymn-common-of-an-apostle.yml`, "Ætérna Christi múnera") and invitatory
|
||
antiphon (`matins-invitatory-common-of-an-apostle.yml`, "Regem Apostolórum Dóminum"), both read
|
||
directly from the reference engine's `Commune/C1.txt` (Latin + English both present there).
|
||
Live-verified: St. Bartholomew (2026-08-24) and St. Andrew (2026-11-30) both now resolve to this
|
||
Common's text for hymn + invitatory.
|
||
|
||
**Not authored, logged as explicit backlog**, same incremental-content pattern as everywhere
|
||
else in this project:
|
||
- Hymn + invitatory Common tiers for: Martyrs (C2), Virgins (C6), Holy Women
|
||
(C7), Dedication (C8), General/All Saints (C9) — all confirmed to have their own distinct
|
||
`[Invit]`/`[Hymnus Matutinum]` text in the reference source, not yet transcribed. Confessors
|
||
(C4) hymn done 2026-08-25, see below — invitatory antiphon still open.
|
||
- Seasonal invitatory-antiphon tier content (Advent/Lent/Passiontide/Paschaltide/Christmastide)
|
||
— mechanism now exists (same `seasonalOfficeSuffix`-keyed lookup the hymn's own long-standing
|
||
seasonal tier already uses), but no seasonal invitatory file is authored yet; every season
|
||
currently still falls through to the plain ferial invitatory antiphon. Confirmed varying
|
||
content exists in the reference source for at least Paschaltide (`Tempora/Pasc0-0.txt`,
|
||
"Surréxit Dóminus vere") and Christmastide (`Tempora/Nat2-0.txt`, "Admirábile nomen Jesu").
|
||
- Seasonal Matins-hymn content itself (Advent/Lent/Passiontide/Paschaltide) — this gap predates
|
||
this pass (already noted in `resolveMatinsHymn`'s own doc comment) and is unchanged here.
|
||
|
||
**Cross-hour antiphon-doubling dedup**: the same idiom (`splitNamedAntiphon` +
|
||
`isDoubleOrHigher(...) ? full : incipit`) was copy-pasted at 11 call sites across 8 hour files
|
||
(matins.ts x4, lauds.ts x2, vespers.ts x2, compline.ts x1, prime/terce/sext/none.ts x1 each).
|
||
Centralized into one new `openingAntiphon(antiphon, winner)` helper in `hours/resolve-common.ts`
|
||
(alongside `splitNamedAntiphon`, which it wraps); every call site now uses it. Pure dedup, no
|
||
behavior change — confirmed by the full existing test suite passing unchanged.
|
||
|
||
**Cross-hour hymn-resolution dedup — partial**: investigated whether hymn resolution across all
|
||
8 hours could collapse into one shared resolver the same way antiphon-doubling did. Found it
|
||
does *not* fit a single universal helper — hymn resolution falls into 4 genuinely different tier
|
||
shapes (Matins's standalone 4-tier hymn-only resolver; Lauds/Vespers's bundled
|
||
hymn+chapter+responsory+versicle resolver; Prime/Compline's fixed-id-plus-`appendDoxology`;
|
||
Terce/Sext/None's flat one-liner with the doxology baked into the static hymn text). Of these,
|
||
only Terce/Sext/None's case was genuinely byte-identical and safe to collapse — done, via a new
|
||
`resolveSelfContainedHymn` helper in `resolve-common.ts`. **Lauds and Vespers's own bundle
|
||
resolvers turned out NOT to be byte-identical either**, despite looking that way at a glance:
|
||
Lauds gates its whole bundle on whether a `LaudsPsalmodyOverride` exists for the winning saint
|
||
(`getPsalmodyOverrideFor(day)?.id`), while Vespers gates purely on duplex-majus+ rank via
|
||
`getOfficeOverrideId` directly (it has no separate per-feast psalmody-override table to check
|
||
against). Collapsing them into one parameterized helper risks silently changing eligibility
|
||
behavior for one of the two hours, so this was deliberately left alone rather than risked — a
|
||
real follow-on refactor, but one that needs an explicit "eligibility" plugin point designed
|
||
first, not a same-day mechanical extraction. Also checked: Matins's own explicit
|
||
`resolveActiveOctave` hymn tier looked at first like it might be redundant with
|
||
`resolveOfficeWinner`'s own octave-folding (which Lauds/Vespers rely on instead of a separate
|
||
octave check) — confirmed it is *not* redundant: `resolveOfficeWinner`'s fold-in only fires for
|
||
`ordinary-feria` days or via `octaveGoverningPrivilegedDay` (which explicitly excludes
|
||
Christmastide's own stacked octaves), so Matins's separate check still covers real cases (e.g.
|
||
Christmas/Stephen/John/Innocents octave days) the other path doesn't reach. Left unchanged.
|
||
|
||
`npm test` (470 tests) and `tsc --noEmit` both pass.
|
||
|
||
### Common-of-a-Confessor Matins hymn authored, Monastic-vs-secular textual variant (2026-08-25)
|
||
|
||
**Bug fixed**: St. Louis (2026-08-25, `common-of-a-confessor-not-bishop`, wins outright as a
|
||
Simplex) was falling through `resolveMatinsHymn`'s Common-of-Saints tier straight to the plain
|
||
ferial hymn, because none of the 5 confessor `common:` ids in use across `data/calendar/saints/*`
|
||
(`common-of-a-confessor` [37 saints], `common-of-a-confessor-bishop` [33], `common-of-a-
|
||
confessor-not-bishop` [6], `common-of-a-confessor-pope` [3], `common-of-a-confessor-doctor` [4])
|
||
had a `matins-hymn-<id>.yml` authored yet — this was the "Confessors (C4)" gap logged in the
|
||
2026-08-24 entry above. All 5 authored now, all sharing one text ("Iste Conféssor Dómini
|
||
sacrátus" / "This is the Lord's own hallowed confessor").
|
||
|
||
**Textual-variant wrinkle, worth remembering generally**: the reference engine's
|
||
`Commune/C4.txt` actually contains two genuinely different Latin recensions of this hymn, not
|
||
just formatting variants — a secular-breviary Urban VIII revision (`[Hymnus Vespera]`: "...
|
||
coléntes... Hac die lætus méruit beátas Scándere sedes") and an older Monastic-track form
|
||
(`[HymnusM Vespera]`: "...sacrátus... Hódie lætus méruit secréta Scándere cæli"), live-verified
|
||
by querying `Pofficium.pl` both ways (St. Louis under `version=Monastic Tridentinum 1617`
|
||
renders `sacrátus`; a Doctor confessor under `version=Divino Afflatu` renders `coléntes` — the
|
||
split is Monastic-vs-secular, *not* doctor-vs-other, despite a Solesmes chant-archive gabc file
|
||
being labeled "pro Doctoribus"). Since vu's Matins is Monastic-1617-structured throughout, the
|
||
`sacrátus` form is correct for all 5 ids alike. Also: the reference engine's own English track
|
||
has only one translation and reuses it for both Latin forms without distinction — sloppily,
|
||
since its concrete imagery (visiting the saint's *tomb*; being helped by his *merits*, not his
|
||
*prayers*) actually only matches the `sacrátus` Latin. Rather than trust that reused pairing,
|
||
the English here is an original translation of the `sacrátus` Latin (kept in the same unrhymed
|
||
Sapphic 11.11.11.5-syllable shape as this project's other hymn translations), so `status.en` is
|
||
`draft` while `status.la` is `verified`. General lesson: when the reference source has multiple
|
||
textual variants of the same hymn, don't assume a single attached translation was written for
|
||
whichever variant happens to be nearest it — check the imagery/vocabulary against each Latin
|
||
candidate before trusting the pairing.
|
||
|
||
### Matins psalmody overrides — Common-of-Several-Martyrs authored, largest remaining category (2026-08-25)
|
||
|
||
Fifth Common category authored for `matins-psalmody-overrides`, same live-verify-a-saint-with-
|
||
no-proper-antiphon method as the previous four: **`common-of-several-martyrs`**, from Ss. Fabian
|
||
and Sebastian (2029-01-20, Duplex, confirmed clean — a Saturday, no Sunday collision — per their
|
||
own saint-file comment). Live-verified (Monastic Tridentinum 1617, `command=prayMatutinum`)
|
||
genuinely distinct from `common-of-a-martyr` (single martyr), not a duplicate: Nocturn 1 = Ps
|
||
1,2,10,14,15,23 (vs 1,2,4,5,8,10), Nocturn 2 = Ps 32,33,45,60,63,78 (vs 14,20,23,63,64,91), and a
|
||
different Nocturn 3 canticle source (Wisdom 3:1-6/3:7-9/10:17-21, one shared "Beáti, qui
|
||
persecutiónem patiúntur" antiphon, vs Sirach 14/15 + Jeremiah 17 + Sirach 31). New scripture
|
||
verses added: `data/scripture/sap-3.yml` gained 3:1-6 (3:7-9 already existed from St. Andrew's
|
||
own canticle); `sap-10.yml`'s existing 10:17-21 reused as-is, no changes needed.
|
||
|
||
This is the **largest single Common category by population** (40 saints/groups carry this
|
||
`common` id per calendar-wide grep, more than `common-of-a-confessor`'s 37 or `common-of-a-
|
||
martyr-bishop`'s 35) — authoring it unlocks real, non-ferial-fallback Matins psalmody for all 40
|
||
at once. Checked every Duplex+ saint/group in the category (only Holy Innocents and Ss. Fabian
|
||
and Sebastian themselves are Duplex+; Holy Innocents already has full proper content of its own,
|
||
Fabian/Sebastian has none) — same as every category before it, no saint is currently eligible
|
||
for its own `saints/*.yml` entry under the proper-only policy.
|
||
|
||
**Wrinkle**: the reference engine's own English track has no translation at all for two of this
|
||
category's antiphons (Nocturn 1's Ps 10 antiphon and Nocturn 3's "Beáti, qui persecutiónem"
|
||
Beatitude antiphon, Matt 5:10) — its rendered English column just repeats the Latin for both.
|
||
Original English translations supplied for both (matching this project's existing Douay-adjacent
|
||
phrasing elsewhere, e.g. the Rule of Benedict's own "persecution for righteousness" wording) —
|
||
flagged inline in the category file's own header comment, not silently passed through as
|
||
`verified` without a note, even though the resolver's `verifiedText` helper doesn't distinguish
|
||
draft from verified at this granularity (a pre-existing mechanism limitation, not new here).
|
||
|
||
`tests/hours/matins.test.ts`'s old ferial-fallback proof (Ss. Fabian and Sebastian, now
|
||
superseded now that their Common has real content) is rewritten as a Common-tier proof; a fresh
|
||
genuine ferial-fallback proof was added instead (St. Anthony, Abbot, `common-of-an-abbot` — still
|
||
unauthored — 2029-01-17, confirmed clean). `npm test` (471 tests), `tsc --noEmit` both pass.
|
||
|
||
**Still open**: Confessor-Not-a-Bishop [6], Confessor-Pope [3], Confessor-Doctor [4], Virgin [11],
|
||
Virgin-Martyr [17], Abbot [16], Holy Woman [3], Widow [6], Angel [5], Evangelist [3], and the
|
||
smaller one-off Commons — none has a category file yet.
|
||
|
||
### Lauds/Vespers chapter-responsory-hymn-versicle bundle: rank-agnostic Common fallback (2026-08-25)
|
||
|
||
**Bug fixed**: St. Louis (2026-08-25, `common-of-a-confessor-not-bishop`, Simplex, wins
|
||
outright) was showing the plain ferial chapter/hymn at Lauds instead of his own Common's —
|
||
`hours/lauds.ts`'s `resolveOffice` only substituted a feast's own proper bundle
|
||
(`lauds-{part}-<id>.yml`), gated at duplex-majus+ (piggybacking eligibility on the Lauds
|
||
psalmody-override table, itself majus+-only by design), with no Common-level fallback at all.
|
||
Same gap existed in `hours/vespers.ts`'s identical `resolveOffice`. Fixed by adding
|
||
`resolve-common.ts`'s `resolveOfficeBundle`: tries the winner's own proper bundle first, then
|
||
falls back to their Common's bundle (`SaintRecord.common`, the same id driving the day
|
||
collect — not `minorHoursCommon`, a deliberately different id for P/T/S/N), eligibility tested
|
||
rank-agnostically via the existing `getMinorHourOverrideId` (any sanctoral winner, not just
|
||
duplex-majus+) — a simplex feast's own or Common's chapter/hymn is real, distinct liturgical
|
||
content, not something only strong feasts are entitled to. Vespers' own chapter lookup
|
||
(`vespersCapitulumForOverride`, which already falls back to the byte-identical Lauds capitulum
|
||
file when no separate Vespers one exists) is passed in and reused unchanged for both the
|
||
proper and the Common id.
|
||
|
||
**Proof authored**: `common-of-a-confessor-not-bishop`'s full Lauds bundle (chapter, short
|
||
responsory, hymn "Jesu coróna célsior", versicle) and Vespers bundle (chapter reused from
|
||
Lauds, short responsory, hymn "Iste Conféssor Dómini sacrátus" — Latin/English text reused
|
||
verbatim from the already-authored Matins hymn file, same Monastic-track recension — versicle),
|
||
live-verified against Divinum Officium (Monastic Tridentinum 1617). Lauds content cross-checked
|
||
byte-identical between St. Louis's own day and St. Nicholas of Tolentino's (`propers: null`,
|
||
clean year), confirming genuine shared Common content. Vespers content, unusually, had to be
|
||
sourced via the reference engine's `votive=C5` ("Commune Confessoris non Pontificis") view
|
||
instead of any real saint's own rendered Second Vespers — every one of this Common's 4 real
|
||
saints (St. Louis, St. Francis, St. Dominic, St. Nicholas of Tolentino) turned out to have
|
||
their own Second Vespers permanently outranked by a fixed adjacent feast (St. Zephyrinus's
|
||
First Vespers, SS. Placid and Companions', Our Lady of the Snows', the Nativity-of-the-BVM
|
||
octave, respectively) — a structural pattern for this category, not a query mistake. Also
|
||
corrected a wrong lead from earlier in this investigation: `CommuneM/C4a.txt`'s file-inheritance
|
||
structure (`@CommuneM/C4`) initially looked like it meant Confessor-Not-Bishop reuses
|
||
Confessor-Bishop's Office bundle wholesale, but the reference engine's own votive-office naming
|
||
(C4 = "Confessoris Pontificis", C5 = "Confessoris non Pontificis", C4a = "Doctoris Pontificis" —
|
||
an unrelated category with a superficially similar filename) settles that these are two
|
||
genuinely separate Common categories with their own distinct hymns ("Jesu Redémptor ómnium" vs
|
||
"Jesu coróna célsior" at Lauds).
|
||
|
||
`npm test` (471 tests), `tsc --noEmit` both pass. **Still open**: the other 10 Common
|
||
categories in the "still open" list above have no Lauds/Vespers bundle authored yet either —
|
||
same honest incremental-content gap, now on a mechanism that no longer silently drops to the
|
||
ferial default for any rank.
|
||
|
||
## Known, deliberate simplifications (not bugs — working as designed)
|
||
|
||
- `getDayCollects`: each collect in a multi-collect day renders as its own
|
||
complete "Let us pray ... Amen." block, not the more compressed real form
|
||
(one "Let us pray," only the last collect's doxology).
|
||
- Suffrage omission (`lauds.ts`'s `suffrages` case) now checks: Double-or-
|
||
higher rank, Christ the King, Advent/Christmastide/Passiontide season,
|
||
any active octave (however low its own rank), Marian Saturday's own
|
||
Cross/BVM filter, and the Cross-only ferial-office check — all
|
||
confirmed directly against **Monastic Tridentinum 1617** itself, not
|
||
just the Tridentine 1906/1910 track the suffrage content and the
|
||
Joseph/Cross-gating logic were sourced from (checked Advent 2026-12-03,
|
||
Christmastide 2026-12-30, Passiontide 2026-03-23, an active octave via
|
||
St. Lawrence's own Aug 11-12, and ordinary Lent 2026-02-20 specifically
|
||
*not* excluded, to confirm it's Passiontide-only, not all of Lent).
|
||
Still not modeled: the Paschaltide *substitution* (real practice
|
||
replaces the whole suffrage set with a single alleluia-form "Suffragium
|
||
Paschale" during Eastertide rather than omitting or showing the
|
||
ordinary set) — flagged, not built.
|
||
- `lauds-preces` (2026-08): real Tridentine 1906/1910 practice restricts
|
||
the fuller ferial Preces to Advent/Lent/Ember days plus Vigils, not
|
||
every ferial day — widened to every ferial-or-vigil day here by
|
||
explicit choice, mirroring Prime's own identical earlier choice (see
|
||
`data/hours/prime.yml`'s header). Also doesn't model Paschaltide's own
|
||
substitution here either, same gap as the suffrages above.
|
||
- `data/calendar/temporal-categories.yml`'s season→category reconstruction
|
||
is from general knowledge of the pre-1955 tradition, not independently
|
||
verified against a primary source for every entry (flagged inline where
|
||
it matters).
|
||
- St. Michael's Sep 29 date — kept as a deliberate simplification despite
|
||
the live engine showing nothing there across 6 tested years (documented
|
||
in `st-michael.yml` and `sanctoral-calendar.yml`).
|