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
+5
View File
@@ -0,0 +1,5 @@
// Precedence/occurrence rules for when a lower-ranked feast is commemorated
// rather than fully displaced by the day's winning feast. Deliberately not
// designed yet — see plan point 5 ("calendar rules are expected to iterate").
// Unused until milestone 4.
export {};
+8
View File
@@ -0,0 +1,8 @@
// Computus (the date of Easter, and everything the temporal cycle hangs off
// of it — Septuagesima, Ash Wednesday, Ascension, Pentecost, Trinity Sunday).
// Not implemented yet: unused until milestone 4 (Lauds/Vespers), which is the
// first hour content that actually varies by season. Deliberately left empty
// rather than half-built ahead of need — see the "content stores" section of
// the plan for why calendar-rule logic is being kept swappable rather than
// front-loaded.
export {};
+6
View File
@@ -0,0 +1,6 @@
// Sanctoral occurrence resolution: given a date, which saint(s) are
// assigned via data/calendar/sanctoral-calendar.yml's day -> saint-id
// mapping, and their rank/propers/common from data/calendar/saints/<id>.yml.
// See calendar/temporal.ts for the separate Easter/fixed-date resolution.
// Unused until milestone 4.
export {};
+23
View File
@@ -0,0 +1,23 @@
import type { LiturgicalDay } from './types';
import { weekdayOf } from './weekday';
/**
* Resolves everything about a given day *except* hour content — weekday,
* season, and any occurring feasts. Season and occurring feasts are stubs
* until calendar/easter.ts and calendar/feasts.ts land at milestone 4;
* hours that only need weekday (Prime, Compline, Terce, Sext, None) can
* already rely on this fully.
*/
export function resolveDay(isoDate: string): LiturgicalDay {
return {
date: isoDate,
weekday: weekdayOf(isoDate),
// Placeholder string, not a real resolution — real season/temporal-id
// lookup (data/calendar/easter-offsets.yml + fixed-date-calendar.yml)
// lands at milestone 4.
season: 'trinitytide',
occurring: [],
};
}
export type { LiturgicalDay, OccurringFeast, Season, Weekday, FeastRank } from './types';
+8
View File
@@ -0,0 +1,8 @@
// Temporal-cycle occurrence resolution: given a date, which temporal-id
// applies (season, proper Sunday/feria), by combining two offset systems —
// data/calendar/easter-offsets.yml (Septuagesima through Trinitytide) and
// data/calendar/fixed-date-calendar.yml (Christmas, Epiphany). Also where
// the Advent-start and Epiphanytide-length wrinkles noted in
// fixed-date-calendar.yml get arbitrated. Depends on calendar/easter.ts for
// the Easter date itself. Unused until milestone 4.
export {};
+46
View File
@@ -0,0 +1,46 @@
export type Weekday =
| 'sunday'
| 'monday'
| 'tuesday'
| 'wednesday'
| 'thursday'
| 'friday'
| 'saturday';
// Deliberately an open string, not a hardcoded union. Which temporal-id a
// given day resolves to — and what it's *called* ("trinitytide" vs "time
// after pentecost") — is a property of data/calendar/easter-offsets.yml and
// fixed-date-calendar.yml, not a compile-time decision. This is what
// actually resolves the "opinionated but configurable" tension: the opinion
// (trinitytide) lives in editable data, not in a TS enum you'd recompile to
// change.
export type Season = string;
// Open-ended on purpose — the actual ranking scheme (double/semidouble/simple,
// or whatever the finalized rank system turns out to be) is a rule decision for
// milestone 4, not a type decision for milestone 0.
export type FeastRank = string;
export interface OccurringFeast {
id: string;
name: string;
rank: FeastRank;
commemorated: boolean;
// Placeholder for the first/second-Vespers overlap wrinkle — unresolved until
// milestone 4 actually needs it.
vespersFrom?: 'today' | 'firstVespersOfTomorrow';
}
export interface LiturgicalDay {
/** ISO date, e.g. "2026-08-09" */
date: string;
weekday: Weekday;
/**
* Real season resolution depends on Easter's date (see calendar/easter.ts,
* not implemented until milestone 4). Until then this is a placeholder and
* must not be trusted by any hour's logic.
*/
season: Season;
/** Always [] until milestone 4 wires up calendar/feasts.ts. */
occurring: OccurringFeast[];
}
+24
View File
@@ -0,0 +1,24 @@
import type { Weekday } from './types';
const WEEKDAYS: readonly Weekday[] = [
'sunday',
'monday',
'tuesday',
'wednesday',
'thursday',
'friday',
'saturday',
];
/**
* @param isoDate "YYYY-MM-DD". Parsed as UTC midnight so the weekday doesn't
* shift depending on the caller's local timezone.
*/
export function weekdayOf(isoDate: string): Weekday {
const date = new Date(`${isoDate}T00:00:00Z`);
const day = WEEKDAYS[date.getUTCDay()];
if (!day) {
throw new Error(`invalid ISO date: ${isoDate}`);
}
return day;
}