Milestone 0/1: shell, calendar/psalter/hours scaffold, Prime
Deploy / deploy (push) Failing after 1m8s
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:
@@ -0,0 +1,6 @@
|
||||
import type { ResolvedOrdo } from './types';
|
||||
|
||||
// Milestone 2. Not built yet.
|
||||
export function resolveOrdo(date: string): ResolvedOrdo {
|
||||
return { hourId: 'compline', date, parts: [], notImplemented: true };
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { HourId, ResolvedOrdo } from './types';
|
||||
import { HOUR_IDS } from './types';
|
||||
import * as prime from './prime';
|
||||
import * as compline from './compline';
|
||||
import * as terce from './terce';
|
||||
import * as sext from './sext';
|
||||
import * as none from './none';
|
||||
import * as lauds from './lauds';
|
||||
import * as vespers from './vespers';
|
||||
import * as matins from './matins';
|
||||
|
||||
type Resolver = (date: string) => ResolvedOrdo;
|
||||
|
||||
// Uniform on purpose — even though terce/sext/none are structurally
|
||||
// identical, callers never branch on which hour they're asking for.
|
||||
const registry: Record<HourId, Resolver> = {
|
||||
prime: prime.resolveOrdo,
|
||||
compline: compline.resolveOrdo,
|
||||
terce: terce.resolveOrdo,
|
||||
sext: sext.resolveOrdo,
|
||||
none: none.resolveOrdo,
|
||||
lauds: lauds.resolveOrdo,
|
||||
vespers: vespers.resolveOrdo,
|
||||
matins: matins.resolveOrdo,
|
||||
};
|
||||
|
||||
export function resolveOrdo(hourId: HourId, date: string): ResolvedOrdo {
|
||||
return registry[hourId](date);
|
||||
}
|
||||
|
||||
export { HOUR_IDS };
|
||||
export type {
|
||||
HourId,
|
||||
HourDefinition,
|
||||
HourPart,
|
||||
ResolvedOrdo,
|
||||
ResolvedPart,
|
||||
ResolvedText,
|
||||
ResolvedVerse,
|
||||
PropersRef,
|
||||
} from './types';
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ResolvedOrdo } from './types';
|
||||
|
||||
// Milestone 4 — first hour needing real sanctoral occurrence + rank/commemoration
|
||||
// resolution (calendar/feasts.ts, calendar/easter.ts). Not built yet.
|
||||
export function resolveOrdo(date: string): ResolvedOrdo {
|
||||
return { hourId: 'lauds', date, parts: [], notImplemented: true };
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { ResolvedOrdo } from './types';
|
||||
|
||||
// Milestone 5, most complex. Needs its own resolution path (reading-candidate
|
||||
// pool + rank/season-driven slotting), not the static parts-array pattern the
|
||||
// other hours use — see the plan's "Matins schema seam" note. Not built yet.
|
||||
export function resolveOrdo(date: string): ResolvedOrdo {
|
||||
return { hourId: 'matins', date, parts: [], notImplemented: true };
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ResolvedOrdo } from './types';
|
||||
|
||||
// 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 };
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { HourDefinition, HourPart, ResolvedOrdo, ResolvedPart, ResolvedText } from './types';
|
||||
import { resolveDay } from '../calendar';
|
||||
import { getPsalmNumbersFor } from '../psalter/distribution';
|
||||
import { getPsalm } from '../psalter';
|
||||
import primeDefinitionData from '../data/hours/prime.yml';
|
||||
|
||||
const primeDefinition = primeDefinitionData as HourDefinition;
|
||||
|
||||
// No propers data store yet (see plan: "content stores on the horizon") — every
|
||||
// fixed textRef resolves as pending until that lands. Keeping this as an
|
||||
// explicit function (rather than inlining) is exactly the seam that gets
|
||||
// swapped out for a real lookup later.
|
||||
function resolveTextRef(): ResolvedText {
|
||||
return { text: {}, status: { la: 'missing', en: 'missing' } };
|
||||
}
|
||||
|
||||
function resolvePart(part: HourPart, psalmNumbers: number[]): ResolvedPart[] {
|
||||
switch (part.kind) {
|
||||
case 'hymn':
|
||||
case 'chapter':
|
||||
case 'responsory':
|
||||
case 'versicle':
|
||||
case 'prayer':
|
||||
return [{ kind: part.kind, text: resolveTextRef() }];
|
||||
case 'variable':
|
||||
// Only 'by-weekday' is meaningful for Prime; by-season/by-feast-rank
|
||||
// don't apply until milestone 4.
|
||||
return psalmNumbers.map((n) => {
|
||||
const psalm = getPsalm(n);
|
||||
return {
|
||||
kind: 'psalm' as const,
|
||||
psalmNumber: n,
|
||||
verses: psalm.verses.map((v) => ({ n: v.n, text: v.text, status: v.status })),
|
||||
};
|
||||
});
|
||||
case 'psalm':
|
||||
return [
|
||||
{
|
||||
kind: 'psalm',
|
||||
psalmNumber: part.psalmNumber,
|
||||
verses: getPsalm(part.psalmNumber).verses.map((v) => ({
|
||||
n: v.n,
|
||||
text: v.text,
|
||||
status: v.status,
|
||||
})),
|
||||
},
|
||||
];
|
||||
case 'canticle':
|
||||
return [{ kind: 'canticle', canticleId: part.canticleId }];
|
||||
case 'lesson':
|
||||
// Prime has no lessons — Matins is where lesson slotting matters.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveOrdo(date: string): ResolvedOrdo {
|
||||
const day = resolveDay(date);
|
||||
const psalmNumbers = getPsalmNumbersFor('prime', day.weekday);
|
||||
const parts = primeDefinition.parts.flatMap((part) => resolvePart(part, psalmNumbers));
|
||||
return { hourId: 'prime', date, parts };
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ResolvedOrdo } from './types';
|
||||
|
||||
// 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 };
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ResolvedOrdo } from './types';
|
||||
|
||||
// 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 };
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ResolvedOrdo } from './types';
|
||||
|
||||
// Milestone 4, alongside Lauds — also has the first/second-Vespers overlap
|
||||
// wrinkle (see OccurringFeast.vespersFrom). Not built yet.
|
||||
export function resolveOrdo(date: string): ResolvedOrdo {
|
||||
return { hourId: 'vespers', date, parts: [], notImplemented: true };
|
||||
}
|
||||
Reference in New Issue
Block a user