Compare commits
2 Commits
54d1d6190e
...
8919497d24
| Author | SHA1 | Date | |
|---|---|---|---|
| 8919497d24 | |||
| 0f84c9e55b |
+51
-37
@@ -18,16 +18,23 @@
|
|||||||
// a *variable* number of readings, not the historical fixed 3 or the
|
// a *variable* number of readings, not the historical fixed 3 or the
|
||||||
// Rule's own "summer" contraction, deliberately not reproduced —
|
// Rule's own "summer" contraction, deliberately not reproduced —
|
||||||
// this app reads in full year-round), the office winner's and every
|
// this app reads in full year-round), the office winner's and every
|
||||||
// commemorated saint's own patristic/hagiographic/Gospel content, the
|
// commemorated saint's own patristic/hagiographic/Gospel content, and
|
||||||
// plain temporal day's own content, and every active octave's own
|
// every active octave's own reading — is gathered into one ordered
|
||||||
// reading — is gathered into one ordered pool (`buildReadingPool`),
|
// pool (`buildReadingPool`), then slotted across however many
|
||||||
// then slotted across however many nocturns the day's psalmody has
|
// nocturns the day's psalmody has (`distributeIntoNocturns`), with no
|
||||||
// (`distributeIntoNocturns`), with no reading kind pinned to a
|
// reading kind pinned to a particular nocturn number. On a 3-nocturn
|
||||||
// particular nocturn number. On a 3-nocturn day the slotting is
|
// day the slotting is front-light: Nocturn 1 gets one reading,
|
||||||
// front-light: Nocturn 1 gets one reading, Nocturn 2 gets one, and
|
// Nocturn 2 gets one, and Nocturn 3 absorbs the rest of the pool,
|
||||||
// Nocturn 3 absorbs the rest of the pool, however large (user, 2026-08)
|
// however large (user, 2026-08) — not an even chunking of the pool. A
|
||||||
// — not an even chunking of the pool. A pool of only one reading total
|
// pool of only one reading total goes in Nocturn 3, not Nocturn 1.
|
||||||
// goes in Nocturn 3, not Nocturn 1.
|
// The plain temporal day's own content (a governing Sunday's Moralia-
|
||||||
|
// in-Job-style patristic homily and its responsory) is pooled only on
|
||||||
|
// a real 3-nocturn day, not reused verbatim on the week's ferias
|
||||||
|
// (user, 2026-09-01: a plain feria showing that Sunday's own
|
||||||
|
// already-read homily and responsory read as a mechanism bug, not a
|
||||||
|
// deliberate rereading) — a 1-nocturn feria draws only on its own
|
||||||
|
// day's content (the reading plan, any saint/octave content specific
|
||||||
|
// to that date).
|
||||||
// - 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/
|
||||||
@@ -462,38 +469,44 @@ function withSingleAntiphon(refs: PsalmRef[], antiphonText: ResolvedText, winner
|
|||||||
* gathered for `day` — the office winner (if sanctoral), every
|
* gathered for `day` — the office winner (if sanctoral), every
|
||||||
* commemorated saint (a transferred-in feast already appears as `day.winner`
|
* commemorated saint (a transferred-in feast already appears as `day.winner`
|
||||||
* once `resolveDay` has applied the transfer, so it needs no separate
|
* once `resolveDay` has applied the transfer, so it needs no separate
|
||||||
* lookup here), the plain temporal id itself (for an ordinary day's own
|
* lookup here), and — only on a real 3-nocturn day (Sunday, or a Duplex+
|
||||||
* patristic content, e.g. a plain Sunday's Moralia-in-Job-style
|
* feast) — the plain temporal id itself (that Sunday's own Moralia-in-
|
||||||
* commentary), and — for dates from the 1st Sunday of August through the
|
* Job-style commentary) plus, for dates from the 1st Sunday of August
|
||||||
* eve of Advent — the calendar-month/week id (`month-week-<id>`,
|
* through the eve of Advent, the calendar-month/week id (`month-week-<id>`,
|
||||||
* calendar/month-week-id.ts's monthWeekId): the real historical Nocturn 2
|
* calendar/month-week-id.ts's monthWeekId): the real historical Nocturn 2
|
||||||
* for the later post-Pentecost Sundays is keyed by civil calendar month,
|
* for the later post-Pentecost Sundays is keyed by civil calendar month,
|
||||||
* not Easter offset (see that function's own header for why), so it's
|
* not Easter offset (see that function's own header for why), so it's
|
||||||
* pooled here as a second, independent source alongside `temporalId`,
|
* pooled here as a second, independent source alongside `temporalId`.
|
||||||
* same dual-key precedent as propers/bible-plan.ts's Dec25-Jan13 stretch —
|
* Restricting both to `threeNocturns` (user, 2026-09-01 bug report: a
|
||||||
* deliberately inclusive, not just the winner, per the user's own "be
|
* plain Tuesday's Matins was reusing the governing Sunday's own Nocturn 3
|
||||||
* generous, not winner-takes-all" instruction (2026-08). */
|
* homily *and* its responsory verbatim) — a ferial 1-nocturn day now only
|
||||||
function nocturnReadingIds(day: LiturgicalDay, temporalId: string, date: string): string[] {
|
* draws on its own day's content (the user's scripture-plan reading, any
|
||||||
|
* saint/octave content specific to that date), not the Sunday's own
|
||||||
|
* already-read homiletic material. */
|
||||||
|
function nocturnReadingIds(day: LiturgicalDay, temporalId: string, date: string, threeNocturns: boolean): string[] {
|
||||||
const ids = new Set<string>();
|
const ids = new Set<string>();
|
||||||
// Not gated to `kind === 'sanctoral'` -- a named temporal override (e.g.
|
// Not gated to `kind === 'sanctoral'` -- a named temporal override (e.g.
|
||||||
// Immaculate Heart of Mary, calendar/movable-feasts.ts's applyMovableFeasts)
|
// Immaculate Heart of Mary, calendar/movable-feasts.ts's applyMovableFeasts)
|
||||||
// has its own authored nocturn-readings file keyed by its own id too,
|
// has its own authored nocturn-readings file keyed by its own id too,
|
||||||
// distinct from the plain governing-Sunday `temporalId` added below.
|
// distinct from the plain governing-Sunday `temporalId` gated below. But
|
||||||
// Harmless to include unconditionally: on an ordinary day `day.winner.id`
|
// on a plain ordinary feria `day.winner.id` *is* that same governing-
|
||||||
// already equals `temporalId`, so the Set just dedupes.
|
// Sunday `temporalId` (the temporal cycle IS the winner), so it must go
|
||||||
|
// through the same `threeNocturns` gate rather than being added
|
||||||
|
// unconditionally.
|
||||||
|
if (day.winner.kind === 'sanctoral' || day.winner.id !== temporalId || threeNocturns) {
|
||||||
ids.add(day.winner.id);
|
ids.add(day.winner.id);
|
||||||
for (const c of day.commemorations) {
|
|
||||||
// Same reasoning as day.winner.id just above: a commemorated *temporal*
|
|
||||||
// identity (an Ember day merely commemorated under a stronger-ranked
|
|
||||||
// saint, e.g. calendar/ember-days.ts's applyEmberDay) has its own real
|
|
||||||
// nocturn-readings content too, not just a commemorated sanctoral one.
|
|
||||||
if (c.kind === 'sanctoral' || c.kind === 'temporal') ids.add(c.id);
|
|
||||||
}
|
}
|
||||||
// The plain temporalId/month-week content is only pooled when the day's
|
for (const c of day.commemorations) {
|
||||||
// own occurrence decision (calendar/commemorations.ts's decideOccurrence)
|
// Same reasoning as day.winner.id just above.
|
||||||
// actually retained the temporal identity in some form: the temporal
|
if (c.kind === 'sanctoral' || (c.kind === 'temporal' && (c.id !== temporalId || threeNocturns))) ids.add(c.id);
|
||||||
// cycle won outright (day.winner.kind === 'temporal' -- a plain ferial/
|
}
|
||||||
// Sunday, or a named temporal override like Christ the King), or it
|
// The plain temporalId/month-week content is the *governing Sunday's own*
|
||||||
|
// Nocturn 2/3 patristic material, real content for that Sunday itself —
|
||||||
|
// only pooled on a real 3-nocturn day, and (as before) only when the
|
||||||
|
// day's own occurrence decision (calendar/commemorations.ts's
|
||||||
|
// decideOccurrence) actually retained the temporal identity in some form:
|
||||||
|
// the temporal cycle won outright (day.winner.kind === 'temporal' -- a
|
||||||
|
// plain Sunday, or a named temporal override like Christ the King), or it
|
||||||
// survives as a commemoration alongside a sanctoral winner. Excluded:
|
// survives as a commemoration alongside a sanctoral winner. Excluded:
|
||||||
// decideOccurrence's `ordinary-feria` branch, where a real feast --
|
// decideOccurrence's `ordinary-feria` branch, where a real feast --
|
||||||
// however low-ranked -- wins with zero commemorations, correctly
|
// however low-ranked -- wins with zero commemorations, correctly
|
||||||
@@ -501,7 +514,8 @@ function nocturnReadingIds(day: LiturgicalDay, temporalId: string, date: string)
|
|||||||
// duplex-2-classis, 2026-08-24 -- his own proper reading has no Nocturn
|
// duplex-2-classis, 2026-08-24 -- his own proper reading has no Nocturn
|
||||||
// 3 content, and without this gate the leftover 13th-Sunday-after-
|
// 3 content, and without this gate the leftover 13th-Sunday-after-
|
||||||
// Pentecost/month-week content wrongly filled Nocturn 3 instead).
|
// Pentecost/month-week content wrongly filled Nocturn 3 instead).
|
||||||
const temporalKept = day.winner.kind === 'temporal' || day.commemorations.some((c) => c.kind === 'temporal');
|
const temporalKept =
|
||||||
|
threeNocturns && (day.winner.kind === 'temporal' || day.commemorations.some((c) => c.kind === 'temporal'));
|
||||||
if (temporalKept) {
|
if (temporalKept) {
|
||||||
ids.add(temporalId);
|
ids.add(temporalId);
|
||||||
const monthWeek = monthWeekId(date);
|
const monthWeek = monthWeekId(date);
|
||||||
@@ -554,7 +568,7 @@ function gospelReadingPart(r: NocturnReading, homily: NocturnReading | undefined
|
|||||||
* own scripture reading first, then each id's authored content in its own
|
* own scripture reading first, then each id's authored content in its own
|
||||||
* file order (patristic commentary typically precedes a Gospel+homily —
|
* file order (patristic commentary typically precedes a Gospel+homily —
|
||||||
* see data/propers/nocturn-readings/*.yml), then active octaves. */
|
* see data/propers/nocturn-readings/*.yml), then active octaves. */
|
||||||
function buildReadingPool(day: LiturgicalDay, temporalId: string, date: string): ResolvedPart[] {
|
function buildReadingPool(day: LiturgicalDay, temporalId: string, date: string, threeNocturns: boolean): ResolvedPart[] {
|
||||||
const parts: ResolvedPart[] = [];
|
const parts: ResolvedPart[] = [];
|
||||||
for (const r of getBiblePlanReadings(temporalId, day.weekday, date)) {
|
for (const r of getBiblePlanReadings(temporalId, day.weekday, date)) {
|
||||||
const responsory = r.responsory ? { text: r.responsory, status: { la: 'verified' as const, en: 'verified' as const } } : undefined;
|
const responsory = r.responsory ? { text: r.responsory, status: { la: 'verified' as const, en: 'verified' as const } } : undefined;
|
||||||
@@ -578,7 +592,7 @@ function buildReadingPool(day: LiturgicalDay, temporalId: string, date: string):
|
|||||||
// Not hardcoded to [2, 3]: Ember days' own nocturn-readings files use
|
// Not hardcoded to [2, 3]: Ember days' own nocturn-readings files use
|
||||||
// `nocturn: 1` (their single-nocturn structure), so every tag present
|
// `nocturn: 1` (their single-nocturn structure), so every tag present
|
||||||
// must be handled, not just the usual Sunday/feast pair.
|
// must be handled, not just the usual Sunday/feast pair.
|
||||||
const ids = nocturnReadingIds(day, temporalId, date);
|
const ids = nocturnReadingIds(day, temporalId, date, threeNocturns);
|
||||||
const byNocturn = new Map<number, ResolvedPart[]>();
|
const byNocturn = new Map<number, ResolvedPart[]>();
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
const readings = getNocturnReadings(id);
|
const readings = getNocturnReadings(id);
|
||||||
@@ -648,7 +662,7 @@ 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 pool = buildReadingPool(day, temporalId, date, threeNocturns);
|
||||||
const [nocturn1Readings, nocturn2Readings, nocturn3Readings] = distributeIntoNocturns(pool, threeNocturns ? 3 : 1);
|
const [nocturn1Readings, nocturn2Readings, nocturn3Readings] = distributeIntoNocturns(pool, threeNocturns ? 3 : 1);
|
||||||
|
|
||||||
// Tenebrae's real rubric: during the Sacred Triduum the whole opening
|
// Tenebrae's real rubric: during the Sacred Triduum the whole opening
|
||||||
|
|||||||
+44
-13
@@ -524,12 +524,12 @@ describe('resolveOrdo("matins", ...) rest-of-year temporal nocturn-readings swee
|
|||||||
}
|
}
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
['2028-12-04', 'advent-1'],
|
['2028-12-03', 'advent-1'],
|
||||||
['2026-12-07', 'advent-2'],
|
['2026-12-07', 'advent-2'],
|
||||||
['2026-12-14', 'advent-3'],
|
['2026-12-14', 'advent-3'],
|
||||||
['2026-12-21', 'advent-4'],
|
['2026-12-21', 'advent-4'],
|
||||||
['2026-01-05', 'christmas-octave-sunday'],
|
['2026-01-05', 'christmas-octave-sunday'],
|
||||||
['2026-01-12', 'post-epiphany-1'],
|
['2026-01-11', 'post-epiphany-1'],
|
||||||
['2026-01-19', 'post-epiphany-2'],
|
['2026-01-19', 'post-epiphany-2'],
|
||||||
['2026-01-26', 'post-epiphany-3'],
|
['2026-01-26', 'post-epiphany-3'],
|
||||||
['2027-11-01', 'post-epiphany-4 (a resumed post-Pentecost-overflow Sunday, not a January date)'],
|
['2027-11-01', 'post-epiphany-4 (a resumed post-Pentecost-overflow Sunday, not a January date)'],
|
||||||
@@ -538,14 +538,14 @@ describe('resolveOrdo("matins", ...) rest-of-year temporal nocturn-readings swee
|
|||||||
['2026-02-02', 'septuagesima'],
|
['2026-02-02', 'septuagesima'],
|
||||||
['2026-02-09', 'sexagesima'],
|
['2026-02-09', 'sexagesima'],
|
||||||
['2026-02-16', 'quinquagesima'],
|
['2026-02-16', 'quinquagesima'],
|
||||||
['2031-03-03', 'lent-1 (not 2026-02-23: that date is really the Vigil of St. Matthias, transferred onto the Sunday and winning outright)'],
|
['2031-03-02', 'lent-1 (not 2026-02-22: that Sunday is really the Vigil of St. Matthias, transferred onto the Sunday and winning outright)'],
|
||||||
['2026-03-02', 'lent-2'],
|
['2026-03-01', 'lent-2'],
|
||||||
['2026-03-09', 'lent-3'],
|
['2026-03-09', 'lent-3'],
|
||||||
['2026-03-16', 'lent-4'],
|
['2026-03-15', 'lent-4'],
|
||||||
['2026-03-23', 'passion-sunday'],
|
['2026-03-22', 'passion-sunday'],
|
||||||
['2027-03-22', 'palm-sunday'],
|
['2027-03-21', 'palm-sunday'],
|
||||||
['2026-04-13', 'easter-octave'],
|
['2026-04-13', 'easter-octave'],
|
||||||
['2026-04-20', 'easter-3'],
|
['2026-04-19', 'easter-3'],
|
||||||
['2026-04-27', 'easter-4'],
|
['2026-04-27', 'easter-4'],
|
||||||
['2026-05-04', 'easter-5'],
|
['2026-05-04', 'easter-5'],
|
||||||
['2026-05-11', 'easter-6'],
|
['2026-05-11', 'easter-6'],
|
||||||
@@ -641,11 +641,42 @@ describe('resolveOrdo("matins", ...) nocturn-reading temporal/month-week suppres
|
|||||||
expect(lessons.some((l) => l.text.text.la?.includes('Bartholomǽus Apóstolus'))).toBe(true);
|
expect(lessons.some((l) => l.text.text.la?.includes('Bartholomǽus Apóstolus'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('a plain ferial weekday with no sanctoral winner (2026-09-04, a gap day in sanctoral-calendar.yml) still pools the plain temporal/month-week readings, unaffected by the gate', () => {
|
// Superseded by the 2026-09-01 fix below: a plain ferial weekday no
|
||||||
const ordo = resolveOrdo('matins', '2026-09-04');
|
// longer pools the plain temporal/month-week readings at all (that
|
||||||
const lessons = ordo.parts.filter((p) => p.kind === 'lesson') as { text: { status: Record<string, string> } }[];
|
// content is the *governing Sunday's own* Nocturn 2/3 material, reserved
|
||||||
expect(lessons.length).toBeGreaterThan(0);
|
// for the Sunday itself), so this gap day's lessons come only from its
|
||||||
expect(lessons.some((l) => l.text.status.la !== 'missing' && l.text.status.en !== 'missing')).toBe(true);
|
// own day's sources -- none authored yet, hence still missing. That's a
|
||||||
|
// real content gap (tracked in TODO.md), not a mechanism bug.
|
||||||
|
it('a plain ferial weekday with no sanctoral winner (2026-09-04, a gap day in sanctoral-calendar.yml, within the 14th-Sunday-after-Pentecost/month-week-091 week) does not pool the plain temporal/month-week readings', () => {
|
||||||
|
const labels = lessonLabels('2026-09-04');
|
||||||
|
expect(labels).not.toContain("St. Augustine, Bishop of Hippo, Book 2 on the Lord's Sermon on the Mount, ch. 14");
|
||||||
|
expect(labels).not.toContain('St. Gregory the Great, Moralia in Job, Book 2, ch. 1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Real bug (2026-09-01 fix): unlike the 2026-08-24 fix above (which only
|
||||||
|
// suppressed the temporal/month-week pool when a real feast won outright),
|
||||||
|
// nocturnReadingIds still pooled that content on a plain 1-nocturn feria
|
||||||
|
// within an ordinary week, because on such a day day.winner.id (added
|
||||||
|
// unconditionally) already *is* the governing Sunday's own temporalId. A
|
||||||
|
// Tuesday's Matins (2026-09-01, within the 14th-Sunday-after-Pentecost
|
||||||
|
// week) wrongly showed that Sunday's own patristic homily and, worse, its
|
||||||
|
// Nocturn 3 responsory verbatim — content that Sunday had already read
|
||||||
|
// three days earlier. Fixed by gating both the plain temporalId/month-week
|
||||||
|
// pool *and* the day.winner.id/commemoration shortcut that duplicated it
|
||||||
|
// on threeNocturns, so a 1-nocturn feria only ever draws on its own day's
|
||||||
|
// content.
|
||||||
|
describe('resolveOrdo("matins", ...) ferial temporal-pool suppression (2026-09-01 fix)', () => {
|
||||||
|
it("a plain Tuesday feria (2026-09-01, within the 14th-Sunday-after-Pentecost week) does not reuse that Sunday's own Nocturn 3 homily or responsory", () => {
|
||||||
|
const ordo = resolveOrdo('matins', '2026-09-01');
|
||||||
|
const lessons = ordo.parts.filter((p) => p.kind === 'lesson') as {
|
||||||
|
label?: string;
|
||||||
|
responsory?: { text: { la: string } };
|
||||||
|
}[];
|
||||||
|
expect(lessons.map((l) => l.label)).not.toContain(
|
||||||
|
"St. Augustine, Bishop of Hippo, Book 2 on the Lord's Sermon on the Mount, ch. 14",
|
||||||
|
);
|
||||||
|
expect(lessons.some((l) => l.responsory?.text.la.includes('Duo Séraphim'))).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user