Carry over unreached Advent bible-plan readings into Christmastide
Deploy / deploy (push) Successful in 1m55s

Advent's own reading plan (advent-1-sunday through advent-4-saturday,
28 rows for a full 4-week Advent) is always longer than real Advent
runs. Advent's last real day is always Dec 23 (Dec 24 is the Vigil of
Christmas's own temporal id), while Advent 1 Sunday lands anywhere
from Nov 27 to Dec 3 -- even the longest possible Advent is one row
short of the full plan, and the shortest is a whole week short. Every
year, at least the plan's last row never reaches its own natural date.

Added adventCarryoverReadings to propers/bible-plan.ts: whatever
doesn't land on its own natural date now carries forward one reading
per day starting Christmas Day, pooled alongside whatever
Christmastide's own calendar-dated rows already have -- never
replacing them. This is the one place in bible-plan.ts that needs
real calendar-year awareness (calendar/temporal.ts's adventStart).

4 new tests covering the shortest (2028) and a long (2027) Advent,
pooling behavior, and the Dec-24 boundary. npm test (2029 passed) and
tsc --noEmit pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGjUyhUZJaSjiniEmnLdak
This commit is contained in:
2026-09-04 19:17:24 -04:00
parent 52052d737e
commit d39359e5ac
3 changed files with 122 additions and 4 deletions
+33
View File
@@ -6022,3 +6022,36 @@ real `Lev 1` responsory text, `palm-sunday`/Sunday resolves real `Num 25-27` tex
audit re-run, clean. Remaining 10, the true final gap: `rom` plus 9 books with no citation audit re-run, clean. Remaining 10, the true final gap: `rom` plus 9 books with no citation
anywhere in Tempora or Sancti (`bar`, Josue, Judices, Ruth, 1-2 Chronicles, Ezra, Nehemiah, anywhere in Tempora or Sancti (`bar`, Josue, Judices, Ruth, 1-2 Chronicles, Ezra, Nehemiah,
Deuteronomy). Deuteronomy).
### Advent reading-plan carryover into Christmastide (2026-09-05)
User-flagged wrinkle in `data/hours/bible-plan/*.yml`'s own Isaiah plan: it's authored as a full
4-week Advent (`advent-1-sunday` through `advent-4-saturday`, 28 rows), but real Advent is never
that long. Advent's last real day is always Dec 23 (Dec 24 is the Vigil of Christmas's own
temporal id, `calendar/temporal-id.ts`), while Advent 1 Sunday itself — the Sunday nearest Nov 30
— falls anywhere from Nov 27 to Dec 3. Even the *longest* possible Advent (Nov 27, 27 real days)
is one short of the full 28-row plan; the *shortest* (Dec 3, 21 real days) is a whole week short.
Every single year, at least the plan's last row never lands on its own natural date — not just
"most years," as the request first put it.
Fixed in `propers/bible-plan.ts`'s new `adventCarryoverReadings`: whatever doesn't land on its own
natural date now carries forward one reading per day starting Christmas Day itself, pooled
alongside whatever Christmastide's own calendar-dated rows already have for that day (never
replacing them) — same "every contributing source pools, none override" design already used
throughout this file and `hours/matins.ts`'s own reading-pool assembly. Computed live from
`calendar/temporal.ts`'s `adventStart(year)` for each date's own year (the one place in this file
that needs real calendar-year awareness, since every other lookup here is year-independent); no
new content authoring needed, since the Isaiah rows were already there, just never reachable on
their own natural date in a short year.
User's own framing accepted as the trade-off: "for advent only, we need to show all of those
chapters" — i.e. every planned reading should surface somewhere, even if that means spilling into
Christmastide, rather than silently dropping the tail end of a short year's Advent plan.
Live-verified across the full spread of possible Advent-1-Sunday dates (`adventStart` for
2020-2035 spans exactly the Nov 27 - Dec 3 range): 2028 (Advent 1 Sunday = Dec 3, the latest/
shortest case) carries all 7 of week 4's Isaiah readings forward across Dec 25-31 in order; 2027/
2032 (Advent 1 Sunday = Nov 28, near-earliest/longest) carry only the last 2. Added 4 permanent
tests to `tests/propers/bible-plan.test.ts` covering both extremes, the pooling-not-replacing
behavior, and that nothing carries over before Christmas Day itself. `npm test` (2029 passed,
+4 new) and `tsc --noEmit` both pass.
+52 -4
View File
@@ -22,6 +22,9 @@ import { resolvePassages, type ScriptureCitation } from './octave-readings';
import { getResponsoryForBook } from './matins-responsories'; import { getResponsoryForBook } from './matins-responsories';
import { getBookIncipit } from './bible-book-incipits'; import { getBookIncipit } from './bible-book-incipits';
import { canonicalBook } from '../scripture'; import { canonicalBook } from '../scripture';
import { adventStart } from '../calendar/temporal';
import { addDays, daysBetween } from '../calendar/date-math';
import type { Weekday } from '../calendar/types';
const GOSPEL_BOOKS = new Set(['matt', 'mark', 'luke', 'john']); const GOSPEL_BOOKS = new Set(['matt', 'mark', 'luke', 'john']);
@@ -154,10 +157,54 @@ for (const mod of Object.values(modules)) {
} }
} }
const WEEKDAYS: Weekday[] = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
/** Advent's own reading plan (`advent-1-sunday` through `advent-4-saturday`,
* 28 rows, one per day of a full 4-week Advent) is longer than Advent
* itself ever actually runs: Advent's last real day is Dec 23 (Dec 24 is
* the Vigil of Christmas's own temporal id, calendar/temporal-id.ts) but
* Advent 1 Sunday falls anywhere from Nov 27 to Dec 3 (the Sunday nearest
* Nov 30) — even in the longest possible Advent (Advent 1 Sunday = Nov 27,
* 27 real days before Dec 24) the 28th planned reading never lands on its
* own natural date, and in the shortest (Advent 1 Sunday = Dec 3, 21 real
* days) the entire last week's worth (7 readings) never does. Rather than
* silently drop that content, whatever didn't land on its own natural date
* carries forward one reading per day starting Christmas Day itself,
* pooled alongside whatever Christmastide's own calendar-dated rows
* already have for that day (never replacing them) — same "every
* contributing source pools, none override" design as everywhere else in
* this file and in hours/matins.ts's own reading-pool assembly. User
* direction, 2026-09-05: "for advent only, we need to show all of those
* chapters," accepting the spillover into Christmastide as the trade-off.
*
* Real day-by-day for `date`'s own year: the pool has no calendar-year
* awareness anywhere else (temporal ids are already year-independent), so
* this is the one place in this file that computes real dates. */
function adventCarryoverReadings(date: string): BiblePlanReading[] {
const year = Number(date.slice(0, 4));
const christmasDay = `${year}-12-25`;
const daysAfterChristmas = daysBetween(christmasDay, date);
if (daysAfterChristmas < 0) return [];
const vigilOfChristmas = `${year}-12-24`;
const start = adventStart(year);
const skippedKeys: string[] = [];
for (let i = 0; i < 28; i++) {
if (addDays(start, i) >= vigilOfChristmas) {
const week = Math.floor(i / 7) + 1;
skippedKeys.push(`advent-${week}-${WEEKDAYS[i % 7]}`);
}
}
const key = skippedKeys[daysAfterChristmas];
return key ? (readingsByTemporalKey.get(key) ?? []) : [];
}
/** Every reading this plan contributes to Matins' pooled reading list for /** Every reading this plan contributes to Matins' pooled reading list for
* `date` — both the (temporalId, weekday) row and the fixed-calendar-date * `date` — the (temporalId, weekday) row, the fixed-calendar-date row (if
* row (if any), pooled together rather than one overriding the other, since * any), and (Christmas Day onward) any Advent reading bumped past its own
* both can genuinely apply to the same date (see this file's header). * natural date by a short Advent that year (see `adventCarryoverReadings`)
* — all pooled together rather than one overriding another, since more
* than one can genuinely apply to the same date (see this file's header).
* Empty array, not undefined, when nothing's authored for this day yet — * 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
@@ -165,5 +212,6 @@ for (const mod of Object.values(modules)) {
export function getBiblePlanReadings(temporalId: string, weekday: string, date: string): BiblePlanReading[] { export function getBiblePlanReadings(temporalId: string, weekday: string, date: string): BiblePlanReading[] {
const byTemporal = readingsByTemporalKey.get(`${temporalId}-${weekday}`) ?? []; const byTemporal = readingsByTemporalKey.get(`${temporalId}-${weekday}`) ?? [];
const byCalendarDate = readingsByCalendarDate.get(date.slice(5)) ?? []; const byCalendarDate = readingsByCalendarDate.get(date.slice(5)) ?? [];
return [...byTemporal, ...byCalendarDate]; const carryover = adventCarryoverReadings(date);
return [...byTemporal, ...byCalendarDate, ...carryover];
} }
+37
View File
@@ -38,3 +38,40 @@ describe('getBiblePlanReadings label uses the same book-alias normalization as v
expect(readings[0]?.label?.la).toBe('Léctio libri Judith'); expect(readings[0]?.label?.la).toBe('Léctio libri Judith');
}); });
}); });
// The Advent reading plan has 28 rows (advent-1-sunday through
// advent-4-saturday, a full 4-week Advent), but real Advent is never that
// long -- its last real day is always Dec 23 (Dec 24 is the Vigil of
// Christmas's own temporal id), while Advent 1 Sunday itself lands anywhere
// from Nov 27 to Dec 3. Even the longest possible Advent (Nov 27) is 27
// real days, one short of the full plan; the shortest (Dec 3) is 21, a
// whole week short. Whatever doesn't land on its own natural date carries
// forward one reading per day starting Christmas Day, pooled alongside
// whatever Christmastide's own calendar-dated rows already have. User
// direction, 2026-09-05: "for advent only, we need to show all of those
// chapters," accepting the spillover into Christmastide as the trade-off.
describe('Advent reading-plan carryover into Christmastide', () => {
it('a short Advent (2028: Advent 1 Sunday = Dec 3, latest possible) carries all 7 of week 4\'s readings forward, one per day', () => {
const isaiahCitations = [25, 26, 27, 28, 29, 30, 31].map(
(day) => getBiblePlanReadings('christmas-octave-sunday', 'monday', `2028-12-${day}`).find((r) => r.citation.la?.startsWith('Isa'))?.citation.la,
);
expect(isaiahCitations).toEqual(['Isa 46-48', 'Isa 49-51', 'Isa 52-54', 'Isa 55-57', 'Isa 58-60', 'Isa 61-63', 'Isa 64-66']);
});
it('a long Advent (2027: Advent 1 Sunday = Nov 28, near-earliest) carries only the last 2 readings forward', () => {
const isaiahCitations = [25, 26, 27, 28].map(
(day) => getBiblePlanReadings('christmas-octave-sunday', 'monday', `2027-12-${day}`).find((r) => r.citation.la?.startsWith('Isa'))?.citation.la,
);
expect(isaiahCitations).toEqual(['Isa 61-63', 'Isa 64-66', undefined, undefined]);
});
it('carried-over readings pool alongside Christmastide\'s own calendar-dated rows, never replacing them', () => {
const readings = getBiblePlanReadings('christmas-octave-sunday', 'friday', '2028-12-25');
expect(readings.map((r) => r.citation.la)).toEqual(['Cant 1', 'Isa 46-48']);
});
it('does not carry anything over before Christmas Day', () => {
const readings = getBiblePlanReadings('vigil-of-christmas', 'thursday', '2028-12-24');
expect(readings.find((r) => r.citation.la?.startsWith('Isa'))).toBeUndefined();
});
});