Milestone 0/1: shell, calendar/psalter/hours scaffold, Prime
Deploy / deploy (push) Failing after 1m8s

Client-side-first PWA (Vite/TS, no backend) per the approved plan: day-
navigable shell listing all 8 hours, Prime fully resolves via the
calendar -> psalter -> ordo pipeline, the other 7 hours are registered
but flagged not-implemented. Sanctoral/temporal calendar data uses a
day -> id indirection layer (saints, easter-offsets, fixed-date-calendar)
so reassigning a feast to a different day is a data edit, not a code
change. Docker (Caddy-serving-static) + Gitea CI workflow scaffolded to
match the eec/drip/bookshop operational pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 15:20:06 -04:00
commit a5dc6502d7
53 changed files with 8634 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
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 }
| { kind: 'psalm'; psalmNumber: number; antiphonRef?: PropersRef }
| { kind: 'canticle'; canticleId: string; antiphonRef?: PropersRef }
| {
kind: 'lesson';
textRef: PropersRef;
nocturn?: number;
lessonSource?: 'historical' | 'continuous-plan';
}
// Unused by Prime/Compline/the little hours — exercised starting milestone 4.
| { kind: 'variable'; resolve: 'by-weekday' | 'by-season' | 'by-feast-rank' };
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: 'psalm'; psalmNumber: number; antiphon?: ResolvedText; verses: ResolvedVerse[] }
| { kind: 'canticle'; canticleId: string; antiphon?: ResolvedText };
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'>>;
}
/**
* 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;
}