Rework Matins readings into a pooled, distributed model
Replaces the fixed Nocturn 1 = plan / Nocturns 2-3 = patristic slotting with one ordered pool (scripture plan, winner/commemorations/temporal id, active octaves) sliced evenly across however many nocturns the day has. Also makes the bible-plan store dual-keyed so the Dec 25 - Jan 13 stretch can key off a fixed calendar date alongside the usual (temporalId, weekday) key. Mechanism only — content authoring is still 2 proof dates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,62 @@ 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
|
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."
|
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:
|
||||||
|
- Pre-1955-but-post-1617 saints' differently-structured Matins readings — source confirmed
|
||||||
|
(Tridentine/Divino Afflatu, combine-where-sensible applies the same way), but the actual
|
||||||
|
sourcing/modeling pass hasn't started — see "Matins ordo" below.
|
||||||
|
- The "which Sunday governs this date" temporal-id algorithm (23rd-Sunday-before-Advent
|
||||||
|
rule, Epiphany-Sunday transfer to the end of the liturgical year, the
|
||||||
|
Saturday-before-Septuagesima 6th-Epiphany-Sunday commemoration edge case) — tabled, not
|
||||||
|
started.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
- Matins: convert the rest of the user's ~390-row bible-plan TSV (2 proof dates converted
|
||||||
|
so far); 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`); once the pre-1955 mechanism gap above is resolved, source those
|
||||||
|
saints' readings too.
|
||||||
|
- 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.
|
||||||
|
- A handful of individually-flagged content gaps, each already flagged in place rather than
|
||||||
|
silently dropped: St. Frances of Rome's and St. Bibiana's Benedictus antiphons
|
||||||
|
(structurally can't win their own day under this app's calendar in any near-term year,
|
||||||
|
needs a very late test year to check); Ss. Ursula and Companions' Benedictus antiphon
|
||||||
|
(a category-of-one, no live rendering to verify against); "Our Lady of the Snows" (not
|
||||||
|
modeled at all); the four "…benedícta…" octave-day Ant+V/R+collect blocks (not yet
|
||||||
|
modeled).
|
||||||
|
|
||||||
|
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
|
## Content gaps — mechanism built, data incomplete
|
||||||
|
|
||||||
### Lauds psalmody overrides + office-bundle overrides — done (2026-08)
|
### Lauds psalmody overrides + office-bundle overrides — done (2026-08)
|
||||||
@@ -1231,11 +1287,41 @@ counts, Te Deum presence, reading citations and statuses, Gospel flagging
|
|||||||
and its Common-fallback exclusion, responsory matching) — not just checking
|
and its Common-fallback exclusion, responsory matching) — not just checking
|
||||||
the YAML files exist. `npm test` (359 tests) and `npm run build` both pass.
|
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:**
|
**Not done in this pass — explicitly deferred, not silently dropped:**
|
||||||
- All ~390 rows of the user's own bible-plan TSV — only the two proof
|
- All ~390 rows of the user's own bible-plan TSV — only the two proof
|
||||||
dates' rows are converted so far.
|
dates' rows are converted so far (both temporal-id-keyed; no
|
||||||
- Full Sunday/feast-calendar patristic-reading authoring (the ~75 Duplex+
|
calendar-date-keyed rows for the Dec 25 - Jan 13 stretch yet).
|
||||||
saints eligible for Nocturn 3, per the user's own threshold).
|
- 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).
|
||||||
- Pre-1955-but-post-1617 saints' differently-structured readings (source
|
- Pre-1955-but-post-1617 saints' differently-structured readings (source
|
||||||
confirmed as Tridentine/Divino Afflatu rather than Monastic 1617,
|
confirmed as Tridentine/Divino Afflatu rather than Monastic 1617,
|
||||||
combine-where-sensible rule confirmed to apply the same way — the actual
|
combine-where-sensible rule confirmed to apply the same way — the actual
|
||||||
|
|||||||
+96
-59
@@ -11,19 +11,33 @@
|
|||||||
// reconstruction. The reference engine's Monastic 1617 data is a content
|
// reconstruction. The reference engine's Monastic 1617 data is a content
|
||||||
// and structure source, not a spec to reproduce — several real, deliberate
|
// and structure source, not a spec to reproduce — several real, deliberate
|
||||||
// departures from it are built in here:
|
// departures from it are built in here:
|
||||||
// - Nocturn 1 is always the user's own continuous scripture-reading plan
|
// - Readings are pool-assembled, not fixed-slotted (user, 2026-08,
|
||||||
// (src/propers/bible-plan.ts), never the historical per-day lectionary
|
// superseding this file's original "Nocturn 1 = plan, Nocturns 2-3 =
|
||||||
// — a *variable* number of readings, not the historical fixed 3 (or 1,
|
// patristic" design): every source that can contribute for the day —
|
||||||
// in the source's own "summer" contraction — deliberately not
|
// the user's own scripture-reading plan (src/propers/bible-plan.ts,
|
||||||
// reproduced here; this app reads in full year-round).
|
// a *variable* number of readings, not the historical fixed 3 or the
|
||||||
|
// Rule's own "summer" contraction, deliberately not reproduced —
|
||||||
|
// this app reads in full year-round), the office winner's and every
|
||||||
|
// commemorated saint's own patristic/hagiographic/Gospel content, the
|
||||||
|
// plain temporal day's own content, and every active octave's own
|
||||||
|
// reading — is gathered into one ordered pool (`buildReadingPool`),
|
||||||
|
// then sliced into however many nocturns the day's psalmody has
|
||||||
|
// (`distributeIntoNocturns`), with no reading kind pinned to a
|
||||||
|
// particular nocturn number.
|
||||||
// - Where the historical office splits one continuous source across
|
// - Where the historical office splits one continuous source across
|
||||||
// several numbered lessons, this app recombines them into one reading
|
// several numbered lessons, this app recombines them into one reading
|
||||||
// (see src/propers/octave-readings.ts's resolvePassages / src/propers/
|
// (see src/propers/octave-readings.ts's resolvePassages / src/propers/
|
||||||
// nocturn-readings.ts) — split only where the underlying source
|
// nocturn-readings.ts) — split only where the underlying source
|
||||||
// genuinely changes (e.g. a Gospel pericope vs. the homily on it).
|
// genuinely changes (e.g. a Gospel pericope vs. the homily on it).
|
||||||
// - Nocturn 3 is gated at Duplex-and-higher (plus every Sunday,
|
// - The *number* of nocturns (1 vs. 3) is still gated at Duplex-and-higher
|
||||||
// unconditionally) — the user's own choice (2026-08), not the
|
// (plus every Sunday, unconditionally) — the user's own choice
|
||||||
// historical Rule's own more permissive threshold.
|
// (2026-08), not the historical Rule's own more permissive threshold.
|
||||||
|
// This is a psalmody-structure decision only; it no longer limits which
|
||||||
|
// days get patristic reading content authored — a sub-Duplex day's
|
||||||
|
// single nocturn can and should include patristic/hagiographic content
|
||||||
|
// from the pool whenever it's been sourced for that day (user, 2026-08:
|
||||||
|
// "patristic readings for every saint where we can source one, not
|
||||||
|
// just duplex+").
|
||||||
// - A Gospel reading is sourced from exactly two places: the user's own
|
// - A Gospel reading is sourced from exactly two places: the user's own
|
||||||
// plan (flagged via BiblePlanReading.isGospel — never present on a
|
// plan (flagged via BiblePlanReading.isGospel — never present on a
|
||||||
// Sunday, a deliberate editorial choice in the user's own plan, not a
|
// Sunday, a deliberate editorial choice in the user's own plan, not a
|
||||||
@@ -34,10 +48,10 @@
|
|||||||
// benedictusCommon elsewhere), so this exclusion falls out of the
|
// benedictusCommon elsewhere), so this exclusion falls out of the
|
||||||
// store's own shape rather than needing special-case code.
|
// store's own shape rather than needing special-case code.
|
||||||
// - Every commemorated saint (not just the office winner) and every
|
// - Every commemorated saint (not just the office winner) and every
|
||||||
// active octave gets its own Nocturn 2/3 contribution, when authored —
|
// active octave contributes its own reading to the pool, when
|
||||||
// "be generous, not winner-takes-all" (user, 2026-08) — mirroring
|
// authored — "be generous, not winner-takes-all" (user, 2026-08) —
|
||||||
// getDayCollects's own "one collect per commemoration" pattern, applied
|
// mirroring getDayCollects's own "one collect per commemoration"
|
||||||
// to readings instead.
|
// pattern, applied to readings instead.
|
||||||
//
|
//
|
||||||
// Only a small, growable slice of content is authored so far (one clean
|
// Only a small, growable slice of content is authored so far (one clean
|
||||||
// ferial day, one clean Sunday) — this is the mechanism build, not the
|
// ferial day, one clean Sunday) — this is the mechanism build, not the
|
||||||
@@ -151,26 +165,14 @@ function ferialPsalmody(day: LiturgicalDay): ResolvedPart[] {
|
|||||||
return psalmRefParts(getPsalmsFor('matins', day.weekday));
|
return psalmRefParts(getPsalmsFor('matins', day.weekday));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Nocturn 1 — always the user's own reading plan, on every kind of day
|
/** Every id whose own patristic/hagiographic/Gospel content should be
|
||||||
* (see this file's header). Empty when nothing's authored for this
|
* gathered for `day` — the office winner (if sanctoral), every
|
||||||
* (temporalId, weekday) yet — an honest absence, not a placeholder. */
|
* commemorated saint (a transferred-in feast already appears as `day.winner`
|
||||||
function nocturn1ReadingParts(temporalId: string, day: LiturgicalDay): ResolvedPart[] {
|
* once `resolveDay` has applied the transfer, so it needs no separate
|
||||||
const readings = getBiblePlanReadings(temporalId, day.weekday);
|
* lookup here), and the plain temporal id itself (for an ordinary day's own
|
||||||
return readings.map((r) => ({
|
* patristic content, e.g. a plain Sunday's Moralia-in-Job-style
|
||||||
kind: 'lesson',
|
* commentary) — deliberately inclusive, not just the winner, per the
|
||||||
text: { text: r.text, status: r.status, citation: r.citation },
|
* user's own "be generous, not winner-takes-all" instruction (2026-08). */
|
||||||
nocturn: 1,
|
|
||||||
isGospel: r.isGospel,
|
|
||||||
responsory: r.responsory ? { text: r.responsory, status: { la: 'verified', en: 'verified' } } : undefined,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Every id whose own Nocturn 2/3 content should be gathered for `day` —
|
|
||||||
* the office winner (if sanctoral), every commemorated saint, and the
|
|
||||||
* plain temporal id itself (for an ordinary day's own patristic content,
|
|
||||||
* e.g. a plain Sunday's Moralia-in-Job-style commentary) — deliberately
|
|
||||||
* inclusive, not just the winner, per the user's own "be generous, not
|
|
||||||
* winner-takes-all" instruction (2026-08). */
|
|
||||||
function nocturnReadingIds(day: LiturgicalDay, temporalId: string): string[] {
|
function nocturnReadingIds(day: LiturgicalDay, temporalId: string): string[] {
|
||||||
const ids = new Set<string>();
|
const ids = new Set<string>();
|
||||||
if (day.winner.kind === 'sanctoral') ids.add(day.winner.id);
|
if (day.winner.kind === 'sanctoral') ids.add(day.winner.id);
|
||||||
@@ -186,44 +188,77 @@ function nocturnReadingPart(r: NocturnReading): ResolvedPart {
|
|||||||
kind: 'lesson',
|
kind: 'lesson',
|
||||||
text: { text: r.text, status: r.status, citation: r.citation },
|
text: { text: r.text, status: r.status, citation: r.citation },
|
||||||
label: r.source,
|
label: r.source,
|
||||||
nocturn: r.nocturn,
|
|
||||||
isGospel: r.isGospel,
|
isGospel: r.isGospel,
|
||||||
responsory: r.responsory ? { text: r.responsory, status: { la: 'verified', en: 'verified' } } : undefined,
|
responsory: r.responsory ? { text: r.responsory, status: { la: 'verified', en: 'verified' } } : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Nocturns 2-3's patristic/hagiographic/Gospel content for a 3-nocturn
|
/** The full pool of readings available for `day` — every source that can
|
||||||
* day — gathered from every relevant id (see nocturnReadingIds) plus every
|
* contribute (see this file's header): the user's own scripture-plan
|
||||||
* currently active octave's own already-authored octave-day reading (see
|
* readings, the office winner's and every commemorated saint's own
|
||||||
* src/propers/octave-readings.ts — built well ahead of this file, never
|
* patristic/hagiographic/Gospel content, the plain temporal id's own
|
||||||
* wired to anything until now). Filtered to the requested nocturn number. */
|
* content (e.g. an ordinary Sunday's patristic commentary), and every
|
||||||
function nocturnReadingParts(nocturn: number, day: LiturgicalDay, temporalId: string, date: string): ResolvedPart[] {
|
* currently active octave's own reading. No source is pinned to a
|
||||||
|
* particular nocturn — `distributeIntoNocturns` slots the whole pool
|
||||||
|
* across however many nocturns the day's psalmody has, per the user's own
|
||||||
|
* "assemble everything, then slot it in" instruction (2026-08), a
|
||||||
|
* deliberate departure from this file's earlier "Nocturn 1 = plan,
|
||||||
|
* Nocturns 2-3 = patristic" design. Order here is preserved by
|
||||||
|
* `distributeIntoNocturns`, so it doubles as reading priority: the user's
|
||||||
|
* own scripture reading first, then each id's authored content in its own
|
||||||
|
* file order (patristic commentary typically precedes a Gospel+homily —
|
||||||
|
* see data/propers/nocturn-readings/*.yml), then active octaves. */
|
||||||
|
function buildReadingPool(day: LiturgicalDay, temporalId: string, date: string): ResolvedPart[] {
|
||||||
const parts: ResolvedPart[] = [];
|
const parts: ResolvedPart[] = [];
|
||||||
|
for (const r of getBiblePlanReadings(temporalId, day.weekday, date)) {
|
||||||
|
parts.push({
|
||||||
|
kind: 'lesson',
|
||||||
|
text: { text: r.text, status: r.status, citation: r.citation },
|
||||||
|
isGospel: r.isGospel,
|
||||||
|
responsory: r.responsory ? { text: r.responsory, status: { la: 'verified', en: 'verified' } } : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
for (const id of nocturnReadingIds(day, temporalId)) {
|
for (const id of nocturnReadingIds(day, temporalId)) {
|
||||||
for (const reading of getNocturnReadings(id)) {
|
for (const reading of getNocturnReadings(id)) {
|
||||||
if (reading.nocturn === nocturn) parts.push(nocturnReadingPart(reading));
|
parts.push(nocturnReadingPart(reading));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nocturn === 2) {
|
for (const octave of activeOctavesFor(date)) {
|
||||||
for (const octave of activeOctavesFor(date)) {
|
const reading = getOctaveReading(octave.id, octave.dayNumber);
|
||||||
const reading = getOctaveReading(octave.id, octave.dayNumber);
|
if (reading) {
|
||||||
if (reading) {
|
parts.push({
|
||||||
parts.push({
|
kind: 'lesson',
|
||||||
kind: 'lesson',
|
text: { text: reading.text, status: reading.status },
|
||||||
text: { text: reading.text, status: reading.status },
|
label: reading.source,
|
||||||
label: reading.source,
|
isGospel: false,
|
||||||
nocturn: 2,
|
responsory: reading.responsory
|
||||||
isGospel: false,
|
? { text: reading.responsory, status: { la: 'verified', en: 'verified' } }
|
||||||
responsory: reading.responsory
|
: undefined,
|
||||||
? { text: reading.responsory, status: { la: 'verified', en: 'verified' } }
|
});
|
||||||
: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Splits `pool` into `nocturnCount` contiguous, as-even-as-possible
|
||||||
|
* chunks (ceiling division), preserving pool order within each chunk —
|
||||||
|
* "depending on how many we have" (user, 2026-08), not a fixed count per
|
||||||
|
* nocturn. A pool of 5 across 3 nocturns lands 2/2/1, not the historical
|
||||||
|
* fixed lesson-count-per-nocturn scheme. */
|
||||||
|
function distributeIntoNocturns(pool: ResolvedPart[], nocturnCount: number): ResolvedPart[][] {
|
||||||
|
const chunks: ResolvedPart[][] = [];
|
||||||
|
const size = Math.ceil(pool.length / nocturnCount);
|
||||||
|
for (let i = 0; i < nocturnCount; i++) {
|
||||||
|
const chunk = pool.slice(i * size, (i + 1) * size);
|
||||||
|
// Stamp each reading with the nocturn slot it actually landed in —
|
||||||
|
// only meaningful once distribution has happened, not while the pool
|
||||||
|
// is still unordered-by-nocturn (see ResolvedPart's own 'lesson'
|
||||||
|
// variant, `nocturn?: number`).
|
||||||
|
chunks.push(nocturnCount > 1 ? chunk.map((part) => ({ ...part, nocturn: i + 1 })) : chunk);
|
||||||
|
}
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveOrdo(date: string): ResolvedOrdo {
|
export function resolveOrdo(date: string): ResolvedOrdo {
|
||||||
const day = resolveDay(date);
|
const day = resolveDay(date);
|
||||||
const winner = resolveOfficeWinner(day);
|
const winner = resolveOfficeWinner(day);
|
||||||
@@ -234,6 +269,8 @@ export function resolveOrdo(date: string): ResolvedOrdo {
|
|||||||
// convention every other per-feast override in this app already uses
|
// convention every other per-feast override in this app already uses
|
||||||
// (see hours/resolve-common.ts's getOfficeOverrideId).
|
// (see hours/resolve-common.ts's getOfficeOverrideId).
|
||||||
const threeNocturns = day.weekday === 'sunday' || isDoubleOrHigher(winner);
|
const threeNocturns = day.weekday === 'sunday' || isDoubleOrHigher(winner);
|
||||||
|
const pool = buildReadingPool(day, temporalId, date);
|
||||||
|
const [nocturn1Readings, nocturn2Readings, nocturn3Readings] = distributeIntoNocturns(pool, threeNocturns ? 3 : 1);
|
||||||
|
|
||||||
const parts: ResolvedPart[] = [
|
const parts: ResolvedPart[] = [
|
||||||
{ kind: 'versicle', text: resolveCommon(getOpeningVersicleId(day.season, day.winner)) },
|
{ kind: 'versicle', text: resolveCommon(getOpeningVersicleId(day.season, day.winner)) },
|
||||||
@@ -244,15 +281,15 @@ export function resolveOrdo(date: string): ResolvedOrdo {
|
|||||||
|
|
||||||
if (threeNocturns) {
|
if (threeNocturns) {
|
||||||
parts.push(...sundayPsalmNocturn(sundayAntiphons.nocturn1, day));
|
parts.push(...sundayPsalmNocturn(sundayAntiphons.nocturn1, day));
|
||||||
parts.push(...nocturn1ReadingParts(temporalId, day));
|
parts.push(...(nocturn1Readings ?? []));
|
||||||
parts.push(...sundayPsalmNocturn(sundayAntiphons.nocturn2, day));
|
parts.push(...sundayPsalmNocturn(sundayAntiphons.nocturn2, day));
|
||||||
parts.push(...nocturnReadingParts(2, day, temporalId, date));
|
parts.push(...(nocturn2Readings ?? []));
|
||||||
parts.push(...sundayCanticleNocturn(sundayAntiphons.nocturn3, day));
|
parts.push(...sundayCanticleNocturn(sundayAntiphons.nocturn3, day));
|
||||||
parts.push(...nocturnReadingParts(3, day, temporalId, date));
|
parts.push(...(nocturn3Readings ?? []));
|
||||||
parts.push({ kind: 'te-deum', text: resolveCommon('te-deum') });
|
parts.push({ kind: 'te-deum', text: resolveCommon('te-deum') });
|
||||||
} else {
|
} else {
|
||||||
parts.push(...ferialPsalmody(day));
|
parts.push(...ferialPsalmody(day));
|
||||||
parts.push(...nocturn1ReadingParts(temporalId, day));
|
parts.push(...(nocturn1Readings ?? []));
|
||||||
parts.push({ kind: 'chapter', text: resolveCommon('matins-capitulum-ferial') });
|
parts.push({ kind: 'chapter', text: resolveCommon('matins-capitulum-ferial') });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+38
-20
@@ -1,20 +1,22 @@
|
|||||||
// The user's own continuous scripture-reading plan for Matins Nocturn 1 —
|
// The user's own continuous scripture-reading plan, one contributor among
|
||||||
// deliberately not the historical per-day lectionary (see hours/matins.ts's
|
// several to Matins' pooled reading list (see hours/matins.ts's header and
|
||||||
// header and TODO.md's Matins section): a personal year-round reading plan,
|
// TODO.md's Matins section) — deliberately not the historical per-day
|
||||||
// keyed by the same (temporalId, weekday) pair every other content store in
|
// lectionary. Most rows key off the same (temporalId, weekday) pair every
|
||||||
// this project already uses to identify a day, sourced from a TSV the user
|
// other content store in this project already uses to identify a day, but
|
||||||
// maintains outside this repo. Replaces the historical Nocturn-1 lesson
|
// the source TSV's Christmastide/Epiphanytide stretch (Dec 25 - Jan 13)
|
||||||
// count entirely — a *variable* number of readings per day, no RB summer
|
// keys off a fixed calendar date (MM-DD) instead, since that stretch is
|
||||||
// contraction (see calendar/temporal-id.ts's resolveTemporalId for the
|
// read straight through regardless of which temporal Sunday governs the
|
||||||
// id scheme this keys off).
|
// day. The two key types can both apply to the same date (e.g. a Sunday
|
||||||
|
// after Epiphany landing inside Jan 1-13) — `getBiblePlanReadings` checks
|
||||||
|
// both and pools whatever each produces, rather than one overriding the
|
||||||
|
// other.
|
||||||
//
|
//
|
||||||
// Only a small, growable subset is authored so far — this is the mechanism
|
// Only a small, growable subset is authored so far — this is the mechanism
|
||||||
// build, not the full-calendar content pass (~390 rows total in the
|
// build, not the full-calendar content pass (~390 rows total in the
|
||||||
// source TSV); see TODO.md for what's deferred. Every entry not yet
|
// source TSV); see TODO.md for what's deferred. Every entry not yet
|
||||||
// authored here simply resolves to no readings (honest absence, not a
|
// authored here simply resolves to no readings (honest absence, not a
|
||||||
// placeholder) — matins.ts falls back to whatever nocturn-2/3 content
|
// placeholder) — matins.ts's reading pool just has nothing from this
|
||||||
// exists for the day, or renders nothing for Nocturn 1 on a day with
|
// source that day.
|
||||||
// neither.
|
|
||||||
import type { LanguageCode, TranslationStatus } from '../psalter/types';
|
import type { LanguageCode, TranslationStatus } from '../psalter/types';
|
||||||
import { resolvePassages, type ScriptureCitation } from './octave-readings';
|
import { resolvePassages, type ScriptureCitation } from './octave-readings';
|
||||||
import { getResponsoryForBook } from './matins-responsories';
|
import { getResponsoryForBook } from './matins-responsories';
|
||||||
@@ -49,8 +51,13 @@ interface BiblePlanReadingRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface BiblePlanDayRecord {
|
interface BiblePlanDayRecord {
|
||||||
temporalId: string;
|
/** Mutually exclusive with `calendarDate` — a row keys off one or the
|
||||||
weekday: string;
|
* other, never both (see this file's header). */
|
||||||
|
temporalId?: string;
|
||||||
|
weekday?: string;
|
||||||
|
/** "MM-DD", for the Dec 25 - Jan 13 stretch that reads straight through
|
||||||
|
* regardless of which temporal Sunday governs the day. */
|
||||||
|
calendarDate?: string;
|
||||||
readings: BiblePlanReadingRecord[];
|
readings: BiblePlanReadingRecord[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,9 +92,10 @@ const modules = import.meta.glob<{ default: BiblePlanDayRecord }>('../data/hours
|
|||||||
eager: true,
|
eager: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const readingsByKey = new Map<string, BiblePlanReading[]>();
|
const readingsByTemporalKey = new Map<string, BiblePlanReading[]>();
|
||||||
|
const readingsByCalendarDate = new Map<string, BiblePlanReading[]>();
|
||||||
for (const mod of Object.values(modules)) {
|
for (const mod of Object.values(modules)) {
|
||||||
const { temporalId, weekday, readings } = mod.default;
|
const { temporalId, weekday, calendarDate, readings } = mod.default;
|
||||||
// Cycles independently per book (not per reading) so two same-day
|
// Cycles independently per book (not per reading) so two same-day
|
||||||
// readings from the same book don't collide on the pool's first entry —
|
// readings from the same book don't collide on the pool's first entry —
|
||||||
// see matins-responsories.ts's own cycling doc comment.
|
// see matins-responsories.ts's own cycling doc comment.
|
||||||
@@ -98,13 +106,23 @@ for (const mod of Object.values(modules)) {
|
|||||||
seenPerBook.set(book, index + 1);
|
seenPerBook.set(book, index + 1);
|
||||||
return resolveReading(record, index);
|
return resolveReading(record, index);
|
||||||
});
|
});
|
||||||
readingsByKey.set(`${temporalId}-${weekday}`, resolved);
|
if (calendarDate) {
|
||||||
|
readingsByCalendarDate.set(calendarDate, resolved);
|
||||||
|
} else {
|
||||||
|
readingsByTemporalKey.set(`${temporalId}-${weekday}`, resolved);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Empty array, not undefined, when nothing's authored for this day yet —
|
/** Every reading this plan contributes to Matins' pooled reading list for
|
||||||
|
* `date` — both the (temporalId, weekday) row and the fixed-calendar-date
|
||||||
|
* row (if any), pooled together rather than one overriding the other, since
|
||||||
|
* both can genuinely apply to the same date (see this file's header).
|
||||||
|
* Empty array, not undefined, when nothing's authored for this day yet —
|
||||||
* every caller already treats "no readings" as a valid, renderable state
|
* every caller already treats "no readings" as a valid, renderable state
|
||||||
* (see hours/matins.ts), so there's no separate "missing entirely" signal
|
* (see hours/matins.ts), so there's no separate "missing entirely" signal
|
||||||
* to preserve here the way getOctaveReading needs `undefined` for. */
|
* to preserve here the way getOctaveReading needs `undefined` for. */
|
||||||
export function getBiblePlanReadings(temporalId: string, weekday: string): BiblePlanReading[] {
|
export function getBiblePlanReadings(temporalId: string, weekday: string, date: string): BiblePlanReading[] {
|
||||||
return readingsByKey.get(`${temporalId}-${weekday}`) ?? [];
|
const byTemporal = readingsByTemporalKey.get(`${temporalId}-${weekday}`) ?? [];
|
||||||
|
const byCalendarDate = readingsByCalendarDate.get(date.slice(5)) ?? [];
|
||||||
|
return [...byTemporal, ...byCalendarDate];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user