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
+69
View File
@@ -0,0 +1,69 @@
import { describe, expect, it } from 'vitest';
import { decideOccurrence, compareFeastClass, isAtLeast } from '../../src/calendar/commemorations';
describe('compareFeastClass / isAtLeast', () => {
it('orders the six ranks low to high', () => {
expect(compareFeastClass('simplex', 'duplex-1-classis')).toBeLessThan(0);
expect(compareFeastClass('duplex-1-classis', 'simplex')).toBeGreaterThan(0);
expect(compareFeastClass('duplex', 'duplex')).toBe(0);
});
it('isAtLeast is inclusive of the threshold itself', () => {
expect(isAtLeast('duplex-majus', 'duplex-majus')).toBe(true);
expect(isAtLeast('duplex', 'duplex-majus')).toBe(false);
});
});
describe('decideOccurrence', () => {
it('temporal wins trivially when nothing is assigned to the date', () => {
expect(decideOccurrence('ordinary-feria', null)).toEqual({ winner: 'temporal', commemorated: false });
expect(decideOccurrence('privileged-sunday', null)).toEqual({ winner: 'temporal', commemorated: false });
});
it('an ordinary feria always yields to any real feast, uncommemorated', () => {
expect(decideOccurrence('ordinary-feria', 'simplex')).toEqual({ winner: 'sanctoral', commemorated: false });
expect(decideOccurrence('ordinary-feria', 'duplex-1-classis')).toEqual({ winner: 'sanctoral', commemorated: false });
});
it('a privileged feria yields to nothing short of the top class', () => {
expect(decideOccurrence('privileged-feria', 'duplex-2-classis')).toEqual({
winner: 'temporal',
commemorated: false,
});
expect(decideOccurrence('privileged-feria', 'duplex-1-classis')).toEqual({
winner: 'temporal',
commemorated: true,
});
});
it('an ordinary Sunday yields outright only to the top class, and is commemorated by the two ranks below it', () => {
expect(decideOccurrence('ordinary-sunday', 'duplex-1-classis')).toEqual({
winner: 'sanctoral',
commemorated: false,
});
expect(decideOccurrence('ordinary-sunday', 'duplex-2-classis')).toEqual({
winner: 'temporal',
commemorated: true,
});
expect(decideOccurrence('ordinary-sunday', 'duplex-majus')).toEqual({
winner: 'temporal',
commemorated: true,
});
expect(decideOccurrence('ordinary-sunday', 'duplex')).toEqual({ winner: 'temporal', commemorated: false });
});
it('a privileged Sunday is never displaced, and only the top two ranks are even commemorated', () => {
expect(decideOccurrence('privileged-sunday', 'duplex-1-classis')).toEqual({
winner: 'temporal',
commemorated: true,
});
expect(decideOccurrence('privileged-sunday', 'duplex-2-classis')).toEqual({
winner: 'temporal',
commemorated: true,
});
expect(decideOccurrence('privileged-sunday', 'duplex-majus')).toEqual({
winner: 'temporal',
commemorated: false,
});
});
});
+2 -2
View File
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest';
import { resolveOrdo } from '../../src/hours';
const MONDAY = '2026-08-10';
const SUNDAY = '2026-08-09';
const MONDAY = '2026-06-15';
const SUNDAY = '2026-06-14';
describe('resolveOrdo("compline", ...)', () => {
it('is implemented, with fixed psalms 4, 90, 133 regardless of weekday', () => {
+1 -1
View File
@@ -5,7 +5,7 @@ import { resolveOrdo } from '../../src/hours';
// whole psalms (1, 2, 6), the first of which (Psalm 1) has real sample
// content in data/psalms/001.yml, so these tests can check real verse data
// without needing every psalm authored.
const MONDAY = '2026-08-10';
const MONDAY = '2026-06-15';
const SUNDAY = '2026-08-09';
describe('resolveOrdo("prime", ...)', () => {