Prime: real content (psalms, Martyrology, Regula) and ordo corrections
Deploy / deploy (push) Failing after 21s
Deploy / deploy (push) Failing after 21s
Pulls in verified content from Divinum Officium rather than placeholders: 17 psalms (2, 6, 7-19, 118, 129), 365 days of the Martyrology, and the full 121-reading Regula cycle, plus the Athanasian Creed. Ordo corrections driven by review against the real engine output: - Capitulum and Preces now pick a Sunday/feast vs. ferial form (calendar/isSundayOrFeast); ferial Preces said every ferial day by choice. - Chapter responsory, hymn doxology, and the opening versicle's Alleluia/Laus tibi all vary by season via a shared resolver (hours/seasonal-propers.ts). - Real Roman Kalends/Nones/Ides Latin dating (calendar/roman-date.ts, verified against 363/365 real Martyrology headings) plus the historical "bis sextus" Feb 29 handling, and the Martyrology's Luna (moon-day) heading (a ported Golden-Number calculation). - Fixed responsory structure (was missing its initial full repeat), weekday psalm antiphons (opening as incipit-or-full by rank, full repeat after the psalms/Creed as its own part, "*" chant mark kept), scripture citations on the capitulum/lectio brevis, and V./R. markers switched from Unicode symbols to plain text for reliable font rendering. - Dropped Pretiosa and the dead-commemoration psalm (129) for time; trimmed section headings down to the ones that are actually named things (Preces, Chapter Office, etc. no longer relabel connective text). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import type { MartyrologyEntry } from './types';
|
||||
import { lunaDay, lunaOrdinalLatin, lunaOrdinalEnglish } from './luna';
|
||||
import { romanDateLatin } from '../calendar/roman-date';
|
||||
|
||||
// One file per calendar day (MM-DD.yml) — only a handful exist so far
|
||||
// (content-authoring the full 366-day Roman Martyrology is a separate,
|
||||
// later task, same as full psalm-text authoring). Missing days resolve as
|
||||
// pending rather than throwing.
|
||||
const modules = import.meta.glob<{ default: MartyrologyEntry }>('../data/martyrology/*.yml', {
|
||||
eager: true,
|
||||
});
|
||||
|
||||
const entries = new Map<string, MartyrologyEntry>();
|
||||
for (const mod of Object.values(modules)) {
|
||||
entries.set(mod.default.monthDay, mod.default);
|
||||
}
|
||||
|
||||
const ENGLISH_MONTHS = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
|
||||
function dayFollowing(isoDate: string): { month: number; day: number; year: number; monthDay: string } {
|
||||
const date = new Date(`${isoDate}T00:00:00Z`);
|
||||
date.setUTCDate(date.getUTCDate() + 1);
|
||||
const month = date.getUTCMonth() + 1;
|
||||
const day = date.getUTCDate();
|
||||
const year = date.getUTCFullYear();
|
||||
return { month, day, year, monthDay: `${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* The Latin heading uses the real Roman Kalends/Nones/Ides date (see
|
||||
* calendar/roman-date.ts); English dates are given in plain Gregorian
|
||||
* form — there's no traditional English equivalent of the Kalends system.
|
||||
*/
|
||||
function heading(month: number, day: number, year: number): { la: string; en: string } {
|
||||
const luna = lunaDay(month, day, year);
|
||||
return {
|
||||
la: `${romanDateLatin(month, day, year)} Luna ${lunaOrdinalLatin(luna)}. Anno Dómini ${year}.`,
|
||||
en: `${ENGLISH_MONTHS[month - 1]} ${day}, ${year} — the ${lunaOrdinalEnglish(luna)} day of the Moon.`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Monastic Prime reads *tomorrow's* Martyrology entry (the announcement of
|
||||
* the next day's saints), not today's — this computes that offset so callers
|
||||
* just pass the ordo's own date.
|
||||
*/
|
||||
export function getMartyrologyEntryFor(isoDate: string): MartyrologyEntry {
|
||||
const { month, day, year, monthDay } = dayFollowing(isoDate);
|
||||
const entry = entries.get(monthDay);
|
||||
if (!entry) {
|
||||
return { monthDay, text: {}, status: { la: 'missing', en: 'missing' } };
|
||||
}
|
||||
|
||||
const head = heading(month, day, year);
|
||||
return {
|
||||
monthDay,
|
||||
text: {
|
||||
la: entry.text.la ? `${head.la}\n\n${entry.text.la}` : entry.text.la,
|
||||
en: entry.text.en ? `${head.en}\n\n${entry.text.en}` : entry.text.en,
|
||||
},
|
||||
status: entry.status,
|
||||
};
|
||||
}
|
||||
|
||||
export type { MartyrologyEntry } from './types';
|
||||
@@ -0,0 +1,130 @@
|
||||
const MONTHSUP = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
|
||||
|
||||
function isLeapYear(year: number): boolean {
|
||||
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
||||
}
|
||||
|
||||
function dateToYdays(day: number, month: number, year: number): number {
|
||||
const monthOffset = MONTHSUP[month - 1] ?? 0;
|
||||
return monthOffset + day + (month > 2 && isLeapYear(year) ? 1 : 0);
|
||||
}
|
||||
|
||||
function lunaTable(yday: number, letter: string): number {
|
||||
const letters = 'abcdefghiklmnpqrstuABCDERFGHMNP';
|
||||
const letterPosition = letters.indexOf(letter) + 1;
|
||||
const cycleDay = yday % 59;
|
||||
const m = yday < 36 ? 30 : ((yday - 35) % 59 || 59) < 29 ? 29 : 30;
|
||||
let i = cycleDay < 36 ? letterPosition : letterPosition - 1;
|
||||
|
||||
if (cycleDay < 36) {
|
||||
if (letterPosition > 25) i -= 1;
|
||||
if (letterPosition === 25 && cycleDay === 35) i += 1;
|
||||
} else if (letterPosition > 25) {
|
||||
i -= 2;
|
||||
}
|
||||
|
||||
if (yday > 58) {
|
||||
if (letterPosition > 25 && cycleDay < 5) i -= 1;
|
||||
if (letterPosition === 26 && cycleDay === 5) i -= 1;
|
||||
}
|
||||
|
||||
return (((i - 1 + cycleDay) % m) + m) % m + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* The traditional Martyrology's "Luna N" (day of the moon) — ported from
|
||||
* Divinum Officium's _luna_day/_luna_table (a Golden-Number/epact-based
|
||||
* lunar calendar algorithm, not a real astronomical calculation). Verified
|
||||
* against 3 known engine outputs: 2026-08-26 -> 13, 2026-12-25 -> 16,
|
||||
* 2027-03-02 -> 24.
|
||||
*/
|
||||
export function lunaDay(month: number, day: number, year: number): number {
|
||||
let lettersForAurea: string;
|
||||
if (year < 1700) {
|
||||
lettersForAurea = 'amDdqGgtNkBbnEerHhu';
|
||||
} else if (year < 1900) {
|
||||
lettersForAurea = 'PlCcpFfsMiAamDdqGgt';
|
||||
} else if (year < 2200) {
|
||||
lettersForAurea = 'NkBbnEerHhuPlCcpRfs';
|
||||
} else {
|
||||
lettersForAurea = 'MiAamDdqGgtNkBbnEer';
|
||||
}
|
||||
|
||||
const aureaNumber = (year % 19) + 1;
|
||||
const letter = lettersForAurea[aureaNumber - 1];
|
||||
if (!letter) {
|
||||
throw new Error(`unreachable: no aurea letter for year ${year}`);
|
||||
}
|
||||
|
||||
let yday = dateToYdays(day, month, year);
|
||||
if (isLeapYear(year) && (month > 2 || (month === 2 && day > 23))) {
|
||||
yday -= 1;
|
||||
}
|
||||
|
||||
let luna = lunaTable(yday, letter);
|
||||
if (aureaNumber === 1 && month === 1 && letter !== 'P' && day + lunaTable(1, letter) < 32) {
|
||||
luna -= 1;
|
||||
}
|
||||
return luna;
|
||||
}
|
||||
|
||||
const LATIN_ORDINALS = [
|
||||
'prima',
|
||||
'secúnda',
|
||||
'tértia',
|
||||
'quarta',
|
||||
'quinta',
|
||||
'sexta',
|
||||
'séptima',
|
||||
'octáva',
|
||||
'nona',
|
||||
'décima',
|
||||
'undécima',
|
||||
'duodécima',
|
||||
'tértia décima',
|
||||
'quarta décima',
|
||||
'quinta décima',
|
||||
'sexta décima',
|
||||
'décima séptima',
|
||||
'duodevicésima',
|
||||
'undevicésima',
|
||||
'vicésima',
|
||||
'vicésima prima',
|
||||
'vicésima secúnda',
|
||||
'vicésima tértia',
|
||||
'vicésima quarta',
|
||||
'vicésima quinta',
|
||||
'vicésima sexta',
|
||||
'vicésima séptima',
|
||||
'vicésima octáva',
|
||||
'vicésima nona',
|
||||
'tricésima',
|
||||
];
|
||||
|
||||
export function lunaOrdinalLatin(n: number): string {
|
||||
const ordinal = LATIN_ORDINALS[n - 1];
|
||||
if (!ordinal) {
|
||||
throw new Error(`luna day out of range: ${n}`);
|
||||
}
|
||||
return ordinal;
|
||||
}
|
||||
|
||||
function englishOrdinalSuffix(n: number): string {
|
||||
if (n > 3 && n < 21) {
|
||||
return 'th';
|
||||
}
|
||||
switch (n % 10) {
|
||||
case 1:
|
||||
return 'st';
|
||||
case 2:
|
||||
return 'nd';
|
||||
case 3:
|
||||
return 'rd';
|
||||
default:
|
||||
return 'th';
|
||||
}
|
||||
}
|
||||
|
||||
export function lunaOrdinalEnglish(n: number): string {
|
||||
return `${n}${englishOrdinalSuffix(n)}`;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { LanguageCode, TranslationStatus } from '../psalter/types';
|
||||
|
||||
/** Keyed by MM-DD — the martyrology repeats every civil year, same as the
|
||||
* sanctoral calendar, and for the same reason (see calendar/types.ts). */
|
||||
export interface MartyrologyEntry {
|
||||
monthDay: string;
|
||||
text: Partial<Record<LanguageCode, string>>;
|
||||
status: Partial<Record<LanguageCode, TranslationStatus>>;
|
||||
}
|
||||
Reference in New Issue
Block a user