calendar: real occurrence/precedence resolution (feast ranks, sanctoral content)

Replaces the FeastRank placeholder with a real ordered FeastClass (the old
pre-1955 six-level scale) and a new TemporalCategory (privileged/ordinary
Sunday/feria). calendar/commemorations.ts's decideOccurrence encodes the
actual precedence rules as explicit, commented branches per category
rather than the reference engine's own opaque numeric weights — this app
targets one ruleset, so the readability trade-off is worth it. Category
membership (data/calendar/temporal-categories.yml) is a best-effort
reconstruction of the pre-1955 tradition, not sourced from a primary text;
expect corrections.

calendar/feasts.ts resolves real sanctoral candidates, and resolveDay()
wires it all together — occurring/temporalCategory are no longer stubs.
isDoubleOrHigher (hours/antiphon.ts) is now a real comparison instead of
a hardcoded false.

Seeded 7 real saints/feasts, each verified directly against Divinum
Officium's own rank data (not reconstructed from memory) as a small,
growable start: St. Lawrence, the Assumption, St. Augustine, the Nativity
of the BVM, St. Michael, All Saints, the Immaculate Conception.
This commit is contained in:
2026-08-10 07:03:28 -04:00
parent ec448bc35b
commit 2ea21f2c89
19 changed files with 456 additions and 54 deletions
+37 -14
View File
@@ -1,22 +1,45 @@
import type { LiturgicalDay } from './types';
import type { LiturgicalDay, OccurringFeast } from './types';
import { weekdayOf } from './weekday';
import { resolveSeason } from './temporal';
import { resolveSeason, resolveTemporalCategory } from './temporal';
import { getSanctoralCandidatesFor } from './feasts';
import { decideOccurrence, compareFeastClass } from './commemorations';
/**
* Resolves everything about a given day *except* hour content — weekday,
* season, and any occurring feasts. Season is real (see
* calendar/temporal.ts); occurring feasts are still a stub until
* calendar/feasts.ts lands (the sanctoral calendar — which saint, if any,
* is kept on a given day, and Double-vs-not ranking — is a separate,
* larger project from temporal-cycle season resolution).
* season, temporal precedence category, and any occurring feast. All real
* now: `occurring` combines calendar/feasts.ts's sanctoral candidates (if
* more than one shares a date, the highest-ranked wins that contest too —
* clashes *among* saints aren't otherwise modeled) with
* calendar/commemorations.ts's occurrence decision. A feast that's fully
* superseded (not even commemorated) doesn't appear in `occurring` at all.
*/
export function resolveDay(isoDate: string): LiturgicalDay {
return {
date: isoDate,
weekday: weekdayOf(isoDate),
season: resolveSeason(isoDate),
occurring: [],
};
const weekday = weekdayOf(isoDate);
const season = resolveSeason(isoDate);
const temporalCategory = resolveTemporalCategory(isoDate, season, weekday);
const candidates = getSanctoralCandidatesFor(isoDate);
let topCandidate = candidates[0] ?? null;
for (const candidate of candidates) {
if (compareFeastClass(candidate.rank, topCandidate!.rank) > 0) {
topCandidate = candidate;
}
}
const occurring: OccurringFeast[] = [];
if (topCandidate) {
const { winner, commemorated } = decideOccurrence(temporalCategory, topCandidate.rank);
if (winner === 'sanctoral' || commemorated) {
occurring.push({
id: topCandidate.id,
name: topCandidate.name,
rank: topCandidate.rank,
commemorated: winner === 'temporal',
});
}
}
return { date: isoDate, weekday, season, temporalCategory, occurring };
}
/**
@@ -31,4 +54,4 @@ export function isSundayOrFeast(day: LiturgicalDay): boolean {
return day.weekday === 'sunday' || day.occurring.length > 0;
}
export type { LiturgicalDay, OccurringFeast, Season, Weekday, FeastRank } from './types';
export type { LiturgicalDay, OccurringFeast, Season, Weekday, FeastClass, TemporalCategory } from './types';