Three separate resolver files (terce.ts/sext.ts/none.ts), not a shared little-hour module — they're structurally identical but kept independent by choice. Ordo verified against Divinum Officium (Monastic Tridentinum 1617): opening versicle, self-contained hymn, one antiphon framing three psalms (no closing repeat, unlike Prime), a capitulum with its own embedded short versicle/response, the day's own collect, and a closing identical across all three hours. Also fills in psalter-distribution.yml's previously-missing Sunday/ Monday/Tuesday psalm entries (Ps 118 sections for Sunday/Monday; Tuesday turns out to share Wednesday/Thursday's native gradual-psalm set, not a distinct one). Friday/Saturday's antiphons are derived from each day's first psalm rather than reusing the native gradual antiphons, since those two days already use a redistributed, non-native psalm set. hours/types.ts gains a 'day-collect' part kind — the first ordo to use the day's own Proper collect rather than a fixed text. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,7 @@ function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
||||
case 'closing-antiphon':
|
||||
case 'by-day-kind':
|
||||
case 'variable':
|
||||
case 'day-collect':
|
||||
throw new Error(`Compline's ordo doesn't support a '${part.kind}' part`);
|
||||
}
|
||||
}
|
||||
|
||||
+63
-5
@@ -1,7 +1,65 @@
|
||||
import type { ResolvedOrdo } from './types';
|
||||
import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart } from './types';
|
||||
import type { LiturgicalDay, Weekday } from '../calendar/types';
|
||||
import { resolveDay } from '../calendar';
|
||||
import { getDayLabel } from '../calendar/day-label';
|
||||
import { getPsalmsFor } from '../psalter/distribution';
|
||||
import { getPsalmVerses } from '../psalter';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { isDoubleOrHigher } from './antiphon';
|
||||
import { resolveCommon, getDayCollect, splitNamedAntiphon } from './resolve-common';
|
||||
import noneDefinitionData from '../data/hours/none.yml';
|
||||
import antiphonsData from '../data/hours/none-antiphons.yml';
|
||||
|
||||
// Milestone 3. Not built yet — will call into hours/shared/little-hour-shared.ts
|
||||
// alongside terce.ts and sext.ts once that's written.
|
||||
export function resolveOrdo(date: string): ResolvedOrdo {
|
||||
return { hourId: 'none', date, parts: [], notImplemented: true };
|
||||
const noneDefinition = noneDefinitionData as HourDefinition;
|
||||
const antiphons = antiphonsData as Record<Weekday, Partial<Record<string, string>>>;
|
||||
|
||||
function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
||||
switch (part.kind) {
|
||||
case 'opening-versicle':
|
||||
return [{ kind: 'versicle', text: resolveCommon(getOpeningVersicleId(day.season, day.winner)) }];
|
||||
case 'hymn':
|
||||
// Self-contained, no appendDoxology — see none-hymn.yml.
|
||||
return [{ kind: 'hymn', text: resolveCommon(part.textRef.id) }];
|
||||
case 'chapter':
|
||||
return [{ kind: 'chapter', text: resolveCommon(part.textRef.id) }];
|
||||
case 'day-collect':
|
||||
return [{ kind: 'prayer', text: getDayCollect(day) }];
|
||||
case 'preces':
|
||||
return [{ kind: 'preces', text: resolveCommon(part.textRef.id), label: part.label }];
|
||||
case 'variable': {
|
||||
// Only 'by-weekday' applies here — one antiphon framing all three
|
||||
// psalms, no closing repeat (unlike Prime): confirmed directly
|
||||
// against the engine, Capitulum follows the third psalm immediately.
|
||||
const psalmRefs = getPsalmsFor('none', day.weekday);
|
||||
const { incipit, full } = splitNamedAntiphon(antiphons[day.weekday]);
|
||||
const opening = isDoubleOrHigher(day.winner) ? full : incipit;
|
||||
return psalmRefs.map((ref, i) => ({
|
||||
kind: 'psalm' as const,
|
||||
psalmNumber: ref.number,
|
||||
antiphon: i === 0 ? opening : undefined,
|
||||
verses: getPsalmVerses(ref.number, ref.verses).map((v) => ({ n: v.n, text: v.text, status: v.status })),
|
||||
}));
|
||||
}
|
||||
// Not used by None.
|
||||
case 'prayer':
|
||||
case 'responsory':
|
||||
case 'versicle':
|
||||
case 'psalm':
|
||||
case 'canticle':
|
||||
case 'lesson':
|
||||
case 'martyrology':
|
||||
case 'rule-reading':
|
||||
case 'by-day-kind':
|
||||
case 'creed':
|
||||
case 'closing-antiphon':
|
||||
case 'nunc-dimittis':
|
||||
case 'marian-antiphon':
|
||||
throw new Error(`None's ordo doesn't support a '${part.kind}' part`);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveOrdo(date: string): ResolvedOrdo {
|
||||
const day = resolveDay(date);
|
||||
const parts = noneDefinition.parts.flatMap((part) => resolvePart(part, day));
|
||||
return { hourId: 'none', date, parts, dayLabel: getDayLabel(day) };
|
||||
}
|
||||
|
||||
+2
-1
@@ -108,9 +108,10 @@ function resolvePart(part: HourPart, date: string, day: LiturgicalDay): Resolved
|
||||
{ kind: 'preces', text: resolveCommon('lesson-close') },
|
||||
];
|
||||
}
|
||||
// Not used by Prime — Compline-only.
|
||||
// Not used by Prime — Compline-only, or Little-Hours-only.
|
||||
case 'nunc-dimittis':
|
||||
case 'marian-antiphon':
|
||||
case 'day-collect':
|
||||
throw new Error(`Prime's ordo doesn't support a '${part.kind}' part`);
|
||||
}
|
||||
}
|
||||
|
||||
+63
-5
@@ -1,7 +1,65 @@
|
||||
import type { ResolvedOrdo } from './types';
|
||||
import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart } from './types';
|
||||
import type { LiturgicalDay, Weekday } from '../calendar/types';
|
||||
import { resolveDay } from '../calendar';
|
||||
import { getDayLabel } from '../calendar/day-label';
|
||||
import { getPsalmsFor } from '../psalter/distribution';
|
||||
import { getPsalmVerses } from '../psalter';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { isDoubleOrHigher } from './antiphon';
|
||||
import { resolveCommon, getDayCollect, splitNamedAntiphon } from './resolve-common';
|
||||
import sextDefinitionData from '../data/hours/sext.yml';
|
||||
import antiphonsData from '../data/hours/sext-antiphons.yml';
|
||||
|
||||
// Milestone 3. Not built yet — will call into hours/shared/little-hour-shared.ts
|
||||
// alongside terce.ts and none.ts once that's written.
|
||||
export function resolveOrdo(date: string): ResolvedOrdo {
|
||||
return { hourId: 'sext', date, parts: [], notImplemented: true };
|
||||
const sextDefinition = sextDefinitionData as HourDefinition;
|
||||
const antiphons = antiphonsData as Record<Weekday, Partial<Record<string, string>>>;
|
||||
|
||||
function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
||||
switch (part.kind) {
|
||||
case 'opening-versicle':
|
||||
return [{ kind: 'versicle', text: resolveCommon(getOpeningVersicleId(day.season, day.winner)) }];
|
||||
case 'hymn':
|
||||
// Self-contained, no appendDoxology — see sext-hymn.yml.
|
||||
return [{ kind: 'hymn', text: resolveCommon(part.textRef.id) }];
|
||||
case 'chapter':
|
||||
return [{ kind: 'chapter', text: resolveCommon(part.textRef.id) }];
|
||||
case 'day-collect':
|
||||
return [{ kind: 'prayer', text: getDayCollect(day) }];
|
||||
case 'preces':
|
||||
return [{ kind: 'preces', text: resolveCommon(part.textRef.id), label: part.label }];
|
||||
case 'variable': {
|
||||
// Only 'by-weekday' applies here — one antiphon framing all three
|
||||
// psalms, no closing repeat (unlike Prime): confirmed directly
|
||||
// against the engine, Capitulum follows the third psalm immediately.
|
||||
const psalmRefs = getPsalmsFor('sext', day.weekday);
|
||||
const { incipit, full } = splitNamedAntiphon(antiphons[day.weekday]);
|
||||
const opening = isDoubleOrHigher(day.winner) ? full : incipit;
|
||||
return psalmRefs.map((ref, i) => ({
|
||||
kind: 'psalm' as const,
|
||||
psalmNumber: ref.number,
|
||||
antiphon: i === 0 ? opening : undefined,
|
||||
verses: getPsalmVerses(ref.number, ref.verses).map((v) => ({ n: v.n, text: v.text, status: v.status })),
|
||||
}));
|
||||
}
|
||||
// Not used by Sext.
|
||||
case 'prayer':
|
||||
case 'responsory':
|
||||
case 'versicle':
|
||||
case 'psalm':
|
||||
case 'canticle':
|
||||
case 'lesson':
|
||||
case 'martyrology':
|
||||
case 'rule-reading':
|
||||
case 'by-day-kind':
|
||||
case 'creed':
|
||||
case 'closing-antiphon':
|
||||
case 'nunc-dimittis':
|
||||
case 'marian-antiphon':
|
||||
throw new Error(`Sext's ordo doesn't support a '${part.kind}' part`);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveOrdo(date: string): ResolvedOrdo {
|
||||
const day = resolveDay(date);
|
||||
const parts = sextDefinition.parts.flatMap((part) => resolvePart(part, day));
|
||||
return { hourId: 'sext', date, parts, dayLabel: getDayLabel(day) };
|
||||
}
|
||||
|
||||
+63
-5
@@ -1,7 +1,65 @@
|
||||
import type { ResolvedOrdo } from './types';
|
||||
import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart } from './types';
|
||||
import type { LiturgicalDay, Weekday } from '../calendar/types';
|
||||
import { resolveDay } from '../calendar';
|
||||
import { getDayLabel } from '../calendar/day-label';
|
||||
import { getPsalmsFor } from '../psalter/distribution';
|
||||
import { getPsalmVerses } from '../psalter';
|
||||
import { getOpeningVersicleId } from './opening-versicle';
|
||||
import { isDoubleOrHigher } from './antiphon';
|
||||
import { resolveCommon, getDayCollect, splitNamedAntiphon } from './resolve-common';
|
||||
import terceDefinitionData from '../data/hours/terce.yml';
|
||||
import antiphonsData from '../data/hours/terce-antiphons.yml';
|
||||
|
||||
// Milestone 3. Not built yet — will call into hours/shared/little-hour-shared.ts
|
||||
// alongside sext.ts and none.ts once that's written.
|
||||
export function resolveOrdo(date: string): ResolvedOrdo {
|
||||
return { hourId: 'terce', date, parts: [], notImplemented: true };
|
||||
const terceDefinition = terceDefinitionData as HourDefinition;
|
||||
const antiphons = antiphonsData as Record<Weekday, Partial<Record<string, string>>>;
|
||||
|
||||
function resolvePart(part: HourPart, day: LiturgicalDay): ResolvedPart[] {
|
||||
switch (part.kind) {
|
||||
case 'opening-versicle':
|
||||
return [{ kind: 'versicle', text: resolveCommon(getOpeningVersicleId(day.season, day.winner)) }];
|
||||
case 'hymn':
|
||||
// Self-contained, no appendDoxology — see terce-hymn.yml.
|
||||
return [{ kind: 'hymn', text: resolveCommon(part.textRef.id) }];
|
||||
case 'chapter':
|
||||
return [{ kind: 'chapter', text: resolveCommon(part.textRef.id) }];
|
||||
case 'day-collect':
|
||||
return [{ kind: 'prayer', text: getDayCollect(day) }];
|
||||
case 'preces':
|
||||
return [{ kind: 'preces', text: resolveCommon(part.textRef.id), label: part.label }];
|
||||
case 'variable': {
|
||||
// Only 'by-weekday' applies here — one antiphon framing all three
|
||||
// psalms, no closing repeat (unlike Prime): confirmed directly
|
||||
// against the engine, Capitulum follows the third psalm immediately.
|
||||
const psalmRefs = getPsalmsFor('terce', day.weekday);
|
||||
const { incipit, full } = splitNamedAntiphon(antiphons[day.weekday]);
|
||||
const opening = isDoubleOrHigher(day.winner) ? full : incipit;
|
||||
return psalmRefs.map((ref, i) => ({
|
||||
kind: 'psalm' as const,
|
||||
psalmNumber: ref.number,
|
||||
antiphon: i === 0 ? opening : undefined,
|
||||
verses: getPsalmVerses(ref.number, ref.verses).map((v) => ({ n: v.n, text: v.text, status: v.status })),
|
||||
}));
|
||||
}
|
||||
// Not used by Terce.
|
||||
case 'prayer':
|
||||
case 'responsory':
|
||||
case 'versicle':
|
||||
case 'psalm':
|
||||
case 'canticle':
|
||||
case 'lesson':
|
||||
case 'martyrology':
|
||||
case 'rule-reading':
|
||||
case 'by-day-kind':
|
||||
case 'creed':
|
||||
case 'closing-antiphon':
|
||||
case 'nunc-dimittis':
|
||||
case 'marian-antiphon':
|
||||
throw new Error(`Terce's ordo doesn't support a '${part.kind}' part`);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveOrdo(date: string): ResolvedOrdo {
|
||||
const day = resolveDay(date);
|
||||
const parts = terceDefinition.parts.flatMap((part) => resolvePart(part, day));
|
||||
return { hourId: 'terce', date, parts, dayLabel: getDayLabel(day) };
|
||||
}
|
||||
|
||||
+7
-1
@@ -87,7 +87,13 @@ export type HourPart =
|
||||
// Salve Regina. See hours/marian-antiphon.ts.
|
||||
| { kind: 'marian-antiphon' }
|
||||
// Unused by Prime/Compline/the little hours — exercised starting milestone 4.
|
||||
| { kind: 'variable'; resolve: 'by-weekday' | 'by-season' | 'by-feast-rank' };
|
||||
| { kind: 'variable'; resolve: 'by-weekday' | 'by-season' | 'by-feast-rank' }
|
||||
// The day's own Proper-of-season/Proper-of-saints collect — resolved by
|
||||
// day, not by textRef. First used by the Little Hours' "Oratio {ex
|
||||
// Proprio de Tempore}" via hours/resolve-common.ts's getDayCollect;
|
||||
// Prime/Compline both still use a fixed collect (`prayer` + textRef).
|
||||
// Renders as a plain 'prayer' ResolvedPart.
|
||||
| { kind: 'day-collect' };
|
||||
|
||||
export interface HourDefinition {
|
||||
id: HourId;
|
||||
|
||||
Reference in New Issue
Block a user