Files
vu/src/hours/types.ts
T
will 4d88568c42 Add Vespers' ferial Preces, sourced from Tridentine 1906/1910
Monastic 1617 has no ferial Preces at Vespers, same gap Lauds already
had — the fuller "Preces feriales Vespera" is byte-for-byte the Lauds
ferial Preces with Psalm 129 swapped for Psalm 50 in the source, live-
verified against the same clean Advent feria already used for Lauds.
The short Sunday/feast form turns out to be identical at both hours too,
so it's reused directly rather than duplicated.

Factors the shared ferial-or-vigil predicate out of hours/lauds.ts into
resolve-common.ts's isFerialOrVigil so both hours' -preces cases resolve
it the same way instead of keeping two copies in sync by hand.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 21:30:01 -04:00

248 lines
13 KiB
TypeScript

export type HourId =
| 'prime'
| 'compline'
| 'terce'
| 'sext'
| 'none'
| 'lauds'
| 'vespers'
| 'matins';
export const HOUR_IDS: readonly HourId[] = [
'prime',
'compline',
'terce',
'sext',
'none',
'lauds',
'vespers',
'matins',
];
/** Reference into one of the proper-text stores (data/propers/**). Not built yet — placeholder shape. */
export interface PropersRef {
source: 'sanctoral' | 'temporal' | 'common';
id: string;
}
export type HourPart =
| { kind: 'hymn' | 'chapter' | 'responsory' | 'versicle' | 'prayer'; textRef: PropersRef }
// A fixed multi-line cluster of versicles/responses/prayers (Preces, the
// monastic "De Officio Capituli" mini-office, the dead-commemoration
// responsory/collect) — these don't decompose usefully into individual
// hymn/chapter/versicle parts, so they're stored and rendered as one block.
// `label`, when given, is shown as a heading (e.g. "Preces", "Pretiosa") —
// omitted for parts that are just connective tissue around a reading
// (a blessing, a closing dialogue) that don't read as their own section.
// `omitOnDouble` drops the whole part on a Double-or-higher feast (real
// Preces are suppressed above a certain rank) — see calendar/
// isDoubleOrHigher for how far that's actually wired up today.
| { kind: 'preces'; textRef: PropersRef; label?: string; omitOnDouble?: true }
| { kind: 'psalm'; psalmNumber: number; verses?: string; antiphonRef?: PropersRef }
| { kind: 'canticle'; canticleId: string; textRef: PropersRef; antiphonRef?: PropersRef }
| {
kind: 'lesson';
textRef: PropersRef;
label?: string;
nocturn?: number;
lessonSource?: 'historical' | 'continuous-plan';
}
// Resolved by date, not by textRef — see src/martyrology. Monastic Prime
// reads *tomorrow's* entry (the announcement of the next day's saints).
| { kind: 'martyrology' }
// Resolved by date via the day->canonical-reading table (RB is read
// through 3x/year) — see src/regula.
| { kind: 'rule-reading' }
// Chooses between two fixed texts by calendar/isSundayOrFeast — e.g.
// Prime's capitulum (1 Tim on Sunday/feasts, Zach on ferias). Renders as
// a plain 'chapter' or 'preces' ResolvedPart per resolvedKind.
| {
kind: 'by-day-kind';
resolvedKind: 'chapter' | 'preces';
label?: string;
omitOnDouble?: true;
sundayOrFeastRef: PropersRef;
ferialRef: PropersRef;
}
// The Athanasian Creed — included on Sundays that aren't a Double-or-higher
// feast (see calendar/isDoubleOrHigher and hours/prime.ts's 'creed' case
// for how far that's actually implemented today).
| { kind: 'creed' }
// Resolved by season — Allelúja normally, Laus tibi Septuagesima through
// Holy Saturday. See hours/opening-versicle.ts.
| { kind: 'opening-versicle' }
// The full antiphon, repeated after the whole psalm group (and the
// Athanasian Creed, on days it's said) — real practice says it once more
// in full here, distinct from the incipit-or-full opening attached to the
// first psalm. See hours/antiphon.ts.
| { kind: 'closing-antiphon' }
// Compline's canticle — fixed year-round (not weekday-variable the way
// Prime's psalm antiphon is), framed by "Salva nos" incipit-or-full
// before and full after, same rank rule as any other antiphon. Resolves
// to a 'canticle' ResolvedPart (with the opening antiphon inline) plus a
// standalone 'antiphon' ResolvedPart for the closing repeat.
| { kind: 'nunc-dimittis' }
// Lauds only. Everything from Ps 66 through the Laudate psalms (148-150)
// inclusive, resolved by weekday — see hours/lauds.ts's resolvePsalmody
// and data/hours/lauds-antiphons.yml. Opaque here on purpose: the real
// shape (a fixed Ps 66 with no antiphon at all, then a weekday-variable
// number of antiphoned psalm groups, then a weekday-variable OT canticle,
// then the Laudate psalms under one more shared antiphon) doesn't fit any
// existing part kind, and hardcoding psalm numbers in lauds.yml can't
// work since they differ by weekday. A duplex-majus-or-higher sanctoral
// winner, or Marian Saturday, substitutes its own proper psalmody
// instead — see hours/lauds-psalmody-overrides.ts's
// getPsalmodyOverrideFor, gated by direct instruction at duplex-majus+
// rather than every rank real historical practice would (most saints
// were added in ways that override the daily psalms — not wanted here).
// Authored per-feast, not per-Common (explicit choice) — known,
// deliberate gap: most duplex-majus+ saints across the year don't have
// one of these authored yet and fall back to the plain weekday default
// until they do (same incremental-content pattern as everywhere else).
| { kind: 'lauds-psalmody' }
// Lauds only. The weekday-resolved chapter/short-responsory/hymn/closing
// versicle bundle that follows the psalmody — see hours/lauds.ts and
// data/propers/common/lauds-{capitulum,responsory,hymn,versicle}-*.yml.
// Expands into 4 ResolvedParts (chapter, responsory, hymn, versicle).
| { kind: 'lauds-office' }
// Vespers only. The weekday-resolved 4-psalm-group psalmody — see
// hours/vespers.ts and data/hours/vespers-antiphons.yml. Opaque for the
// same reason as 'lauds-psalmody': each group's antiphon covers either
// one whole psalm, two whole psalms said together under one shared
// antiphon (Ps 115+116, Monday), or two verse-ranges of one psalm split
// under one shared antiphon (Ps 138/143, Thu/Fri) — no existing part
// kind represents that. No per-feast override mechanism yet (unlike
// Lauds' getPsalmodyOverrideFor) — every day renders the plain weekday
// default for now.
| { kind: 'vespers-psalmody' }
// Vespers only. The weekday-resolved chapter/short-responsory/hymn/
// versicle bundle that follows the psalmody — see hours/vespers.ts and
// data/propers/common/vespers-{capitulum,responsory,hymn,versicle}-*.yml.
// Expands into 4 ResolvedParts (chapter, responsory, hymn, versicle).
| { kind: 'vespers-office' }
// Vespers only. Magnificat (Luke 1:46-55), fixed text, framed by a
// day-resolved antiphon — see hours/resolve-common.ts's
// getMagnificatAntiphon, which mirrors getBenedictusAntiphon's
// saint/temporal-id lookup but also falls back to a real weekday
// default (data/hours/vespers-magnificat-antiphons.yml, Mon-Sat) that
// Benedictus has no equivalent of. Same incipit-or-full/full-after
// framing as benedictus/nunc-dimittis, gated by isDoubleOrHigher.
| { kind: 'magnificat' }
// Lauds only. Benedictus (Luke 1:68-79), fixed text (see propers/common/
// benedictus.yml) framed by a day-resolved antiphon (see hours/
// resolve-common.ts's getBenedictusAntiphon) — same incipit-or-full/
// full-after-Creed... no Creed at Lauds, just before+after framing shape
// as nunc-dimittis, gated the same way by isDoubleOrHigher.
| { kind: 'benedictus' }
// Lauds only. The four fixed Tridentine suffrages (Holy Cross, BVM,
// Ss. Peter & Paul, Peace) said after the day's collect(s) — see
// data/propers/common/lauds-suffrage-*.yml. `omitOnDouble` gates the
// whole set at once (real practice: suffrages drop on a Double-or-higher
// feast) — an honest simplification, see hours/lauds.ts for the caveat
// on what real practice also suppresses them for that isn't modeled.
| { kind: 'suffrages'; omitOnDouble?: true }
// Lauds only. The Tridentine 1906/1910 "Preces feriales" litany (Kyrie,
// Pater noster, a long chain of versicles, Psalm 129, closing versicles)
// -- content Monastic 1617 doesn't have at Lauds at all, sourced from a
// different track per direct instruction, same as lauds-suffrage-
// joseph.yml. Real 1906/1910 practice restricts this further still
// (Advent/Lent/Ember days, or an explicit day's own rubric flag) --
// deliberately widened here to *every* ferial or vigil day, mirroring
// the same explicit choice already made for Prime's own ferial Preces
// (see data/hours/prime.yml's header). Replaces `lauds-short-litany` on
// those days rather than stacking with it -- see hours/lauds.ts's
// 'lauds-preces' case for the exact ferial-or-vigil predicate, which is
// deliberately not the shared `isSundayOrFeast` (a Vigil counts as a
// "feast" there; here it needs to count as ferial-like instead).
| { kind: 'lauds-preces' }
// Vespers only. Mirrors 'lauds-preces' exactly — the Tridentine
// 1906/1910 "Preces feriales Vespera" (source: `@:Preces feriales
// Laudes:s/129/50/` — byte-for-byte the Lauds ferial Preces with Psalm
// 129 swapped for Psalm 50, live-verified against a clean Advent feria
// 2026-12-17) on a ferial or vigil day, replacing the short Sunday/feast
// litany otherwise said before the day's collect(s) — see hours/
// resolve-common.ts's isFerialOrVigil (shared with 'lauds-preces') and
// hours/vespers.ts's 'vespers-preces' case. Unlike Lauds, the short form
// here reuses `lauds-short-litany` directly rather than a Vespers-named
// duplicate — confirmed byte-identical by live query at both hours,
// same reuse discipline as data/hours/vespers.yml's `lauds-conclusio`
// closing.
| { kind: 'vespers-preces' }
// Lauds/Vespers. The day's own collect *and* one more per commemoration
// (calendar/types.ts's LiturgicalDay.commemorations) — see hours/
// resolve-common.ts's getDayCollects. Distinct from the singular
// 'day-collect' the Little Hours use (which only ever renders the
// winner's own collect, never a commemoration's). Simplification: each
// collect renders as its own complete "Let us pray ... Amen." block
// rather than the more compressed real form (one "Let us pray," only the
// last collect closes with the full doxology) — see hours/lauds.ts.
| { kind: 'day-collects' }
// Compline's closing Marian antiphon (with its own versicle and collect
// baked into the same text block) — resolved by season, defaulting to
// 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' }
// 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;
parts: HourPart[];
}
/** A part with its text actually filled in, ready for the UI to render. */
export type ResolvedPart =
| { kind: 'hymn' | 'chapter' | 'responsory' | 'versicle' | 'prayer'; text: ResolvedText }
| { kind: 'preces'; text: ResolvedText; label?: string }
| { kind: 'psalm'; psalmNumber: number; antiphon?: ResolvedText; verses: ResolvedVerse[] }
| { kind: 'canticle'; canticleId: string; text: ResolvedText; antiphon?: ResolvedText }
// A standalone antiphon, said on its own rather than framing a psalm —
// see 'closing-antiphon' above.
| { kind: 'antiphon'; text: ResolvedText }
// Martyrology and Regula both render as a block of prose text with a
// label (a fixed section name for Martyrology, a date-derived chapter
// title for Regula) rather than a fixed textRef.
| { kind: 'lesson'; text: ResolvedText; label?: string };
export interface ResolvedVerse {
n: number;
text: Partial<Record<string, string>>;
status: Partial<Record<string, 'verified' | 'draft' | 'missing'>>;
}
export interface ResolvedText {
text: Partial<Record<string, string>>;
status: Partial<Record<string, 'verified' | 'draft' | 'missing'>>;
/** e.g. "1 Tim 1:17" — set when the text is a scripture quotation. Always
* given in full (book, chapter, verse) rather than just a book name. */
citation?: Partial<Record<string, string>>;
}
/**
* The "ordo" for one hour on one day — the fully resolved output of
* combining calendar resolution, the psalter distribution, and an
* HourDefinition. This is the seam between the engine and the UI: the UI
* never needs to know whether a part came from a static YAML row or a
* resolved rule.
*/
export interface ResolvedOrdo {
hourId: HourId;
date: string;
parts: ResolvedPart[];
/** Set when this hour hasn't been built yet — UI shows "coming soon" instead of empty content. */
notImplemented?: true;
/**
* "Monday in the 10th week after Trinity", "St. Ereden — ...", etc. —
* see calendar/day-label.ts. Computed from whichever LiturgicalDay the
* hour actually resolved against, which can differ from `date` itself
* (Compline may anticipate tomorrow — see calendar/vespers.ts). Absent
* on not-yet-built hours.
*/
dayLabel?: string;
}