Give each half of Saturday's split Deuteronomy canticle its own citation
Deploy / deploy (push) Successful in 1m27s

canticleText() always returned the whole canticle's stored citation
("Deut 32:1-65"), even when only slicing out half its verses -- both
Saturday Lauds pieces showed the same full range regardless of which
half was actually being said. Derive the citation from the slice's own
first/last verse numbers instead, only when slicing; a full canticle's
stored citation is left as-is, since it already matches what's shown
and a formula can't reproduce an irregular one like
canticum-trium-puerorum's "Dan 3:57-88,56" (a non-contiguous liturgical
splice).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018sPkfGLiq58pmmDoBWH5UX
This commit is contained in:
2026-08-29 10:37:39 -04:00
parent 7153a846c2
commit 4e1266d4ae
2 changed files with 35 additions and 2 deletions
+27 -2
View File
@@ -3,7 +3,7 @@ import type { LiturgicalDay, Weekday } from '../calendar/types';
import { resolveDay } from '../calendar';
import { getDayLabel } from '../calendar/day-label';
import { getPsalmVerses } from '../psalter';
import { getCanticle } from './lauds-canticles';
import { getCanticle, type CanticleVerse } from './lauds-canticles';
import { getLaudsSaintOverride, getLaudsCommonOverride, type LaudsPsalmodyOverride } from './lauds-psalmody-overrides';
import { getOpeningVersicleId } from './opening-versicle';
import { getMarianAntiphonId, getMarianAntiphonLabel } from './marian-antiphon';
@@ -145,6 +145,30 @@ function psalmParts(numbers: number[], antiphon: BilingualText, opening: Resolve
const STATUS_RANK = { verified: 0, draft: 1, missing: 2 } as const;
/** The general rule is that a citation must match whatever verses are
* actually being displayed — not "the whole canticle's citation,
* always." A full canticle's stored `citation` already satisfies that
* (canticleText below only calls this helper for a `slice`), including
* cases a formula couldn't reconstruct — canticum-trium-puerorum's
* "Dan 3:57-88,56" is a non-contiguous liturgical splice, its last verse
* (`n: "3:56"`) coming after verse 88 in display order, not after it
* numerically. Only Saturday's split Deuteronomy canticle ever shows
* fewer than all of a canticle's verses, so it's the only case needing
* this: book name from the canticle's base citation (its first word),
* verse range from the slice's own first/last `n` values (e.g.
* "32:1"/"32:32" -> "Deut 32:1-32") — showing the full "1-65" range on
* both halves would mislabel whichever half isn't actually being said. */
function sliceCitation(baseCitation: string, verses: CanticleVerse[]): string {
const book = baseCitation.split(' ')[0] ?? baseCitation;
const first = verses[0]?.n;
const last = verses[verses.length - 1]?.n;
if (!first || !last) {
return baseCitation;
}
const lastVerse = last.split(':')[1] ?? last;
return `${book} ${first}-${lastVerse}`;
}
/** Joins a canticle's verse array (or a `[start, end)` slice of it — see
* Saturday's split canticle below) into one flowing text block, the same
* shape nunc-dimittis/benedictus already use — Lauds' weekday canticles
@@ -174,7 +198,8 @@ function canticleText(canticleId: string, slice?: [number, number]): ResolvedTex
}
status[lang] = worst;
}
return { text, status, citation: { la: canticle.citation, en: canticle.citation } };
const citation = slice ? sliceCitation(canticle.citation, verses) : canticle.citation;
return { text, status, citation: { la: citation, en: citation } };
}
/** Ps 66 through the Laudate psalms — see hours/types.ts's 'lauds-psalmody'
+8
View File
@@ -56,6 +56,10 @@ describe('resolveOrdo("lauds", ...)', () => {
const canticle = ordo.parts.find((p) => p.kind === 'canticle' && p.canticleId !== 'benedictus');
expect(canticle?.kind === 'canticle' ? canticle.canticleId : undefined).toBe('canticum-trium-puerorum');
// Its non-contiguous liturgical citation (last verse said out of
// numeric order) must come through as authored, not get mangled by
// a first/last-verse-number formula the way a naive fix would.
expect(canticle?.kind === 'canticle' ? canticle.text.citation?.la : undefined).toBe('Dan 3:57-88,56');
});
it("resolves Saturday's own psalmody: only one weekday psalm (142), and the Canticle of Moses said in two pieces (RB 13's own division), one shared antiphon framing both", () => {
@@ -78,6 +82,10 @@ describe('resolveOrdo("lauds", ...)', () => {
expect(part2?.kind === 'canticle' ? part2.antiphon : 'should be undefined -- shares part 1s opening antiphon').toBeUndefined();
expect(part1?.kind === 'canticle' ? part1.text.text.la : undefined).toContain('Audíte, cæli');
expect(part2?.kind === 'canticle' ? part2.text.text.la : undefined).toContain('Ignis succénsus est');
// Regression test: each half must cite its own verse range, not the
// whole canticle's "Deut 32:1-65" on both pieces.
expect(part1?.kind === 'canticle' ? part1.text.citation?.la : undefined).toBe('Deut 32:1-32');
expect(part2?.kind === 'canticle' ? part2.text.citation?.la : undefined).toBe('Deut 32:33-65');
// Each part gets its own Gloria Patri.
expect(part1?.kind === 'canticle' ? part1.gloriaPatri?.text.la : undefined).toContain('Glória Patri');
expect(part2?.kind === 'canticle' ? part2.gloriaPatri?.text.la : undefined).toContain('Glória Patri');