calendar: split privileged-feria into two real precedence tiers
Deploy / deploy (push) Successful in 41s
Deploy / deploy (push) Successful in 41s
A plain Duplex saint was losing to any privileged feria (Ember days included) since the occurrence engine only let duplex-1-classis+ get even a commemoration there. Verified this was wrong: St. Gregory the Great, plain Duplex, outright wins against a Lenten Ember Saturday in the real Monastic 1617 engine. Splits TemporalCategory's single privileged-feria into privileged-feria (the lesser tier — Lent's Ember days, verified — now behaves like an ordinary Sunday: Duplex+ wins outright) and privileged-feria-major (Ash Wednesday, Holy Week, the Easter Octave, verified via St. Mark only ever being commemorated there, never winning — behaves like privileged- Sunday). Everywhere else the old single category was ambiguous (Pentecost's own Ember days/Vigil/Octave, the Vigil of Christmas) defaults to the stricter major tier pending verification. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,10 +8,14 @@
|
|||||||
// Modeled as explicit rules per category, not a numeric-weight comparison
|
// Modeled as explicit rules per category, not a numeric-weight comparison
|
||||||
// like the reference engine's occurrence()/concurrence() — see
|
// like the reference engine's occurrence()/concurrence() — see
|
||||||
// data/calendar/temporal-categories.yml's header for why. Verified in part
|
// data/calendar/temporal-categories.yml's header for why. Verified in part
|
||||||
// against real dates (the Sunday-vs-Duplex threshold below was corrected
|
// against real dates: the Sunday-vs-Duplex threshold was corrected after
|
||||||
// after finding St. Anthony Abbot, plain Duplex, outright winning against
|
// finding St. Anthony Abbot, plain Duplex, outright winning against an
|
||||||
// an ordinary Sunday in the real Monastic 1617 engine — a genuine
|
// ordinary Sunday in the real Monastic 1617 engine, and privileged-feria
|
||||||
// correction, not a guess); the rest is the result of a design discussion,
|
// was later split into two real tiers after finding St. Gregory the Great,
|
||||||
|
// also plain Duplex, outright winning against a Lenten Ember Saturday
|
||||||
|
// (which the original single-tier model wrongly forbade) while St. Mark,
|
||||||
|
// Duplex II. classis, only ever gets commemorated — never wins outright —
|
||||||
|
// within the Easter Octave. The rest is the result of a design discussion,
|
||||||
// not yet independently verified against a primary source.
|
// not yet independently verified against a primary source.
|
||||||
|
|
||||||
import type { Commemoration, DayWinner, FeastClass, SanctoralIdentity, TemporalCategory } from './types';
|
import type { Commemoration, DayWinner, FeastClass, SanctoralIdentity, TemporalCategory } from './types';
|
||||||
@@ -81,11 +85,33 @@ export function decideOccurrence(
|
|||||||
return { winner: sanctoralWinner(sanctoral), commemorations: [] };
|
return { winner: sanctoralWinner(sanctoral), commemorations: [] };
|
||||||
|
|
||||||
case 'privileged-feria':
|
case 'privileged-feria':
|
||||||
// Preferred to any feast whatsoever; admits no commemoration except
|
// The lesser of the two privileged-feria tiers (Ember days outside
|
||||||
// one of the very highest class.
|
// Holy Week/the Easter Octave, etc.) — behaves exactly like an
|
||||||
|
// ordinary Sunday: real standing of its own, but not enough to
|
||||||
|
// resist a sufficiently ranked feast the way privileged-feria-major
|
||||||
|
// does.
|
||||||
|
if (isAtLeast(sanctoral.rank, 'duplex')) {
|
||||||
|
return { winner: sanctoralWinner(sanctoral), commemorations: [{ kind: 'temporal', id: temporalId }] };
|
||||||
|
}
|
||||||
|
if (sanctoral.rank === 'simplex') {
|
||||||
|
return { winner: temporalWinner, commemorations: [sanctoralCommemoration(sanctoral)] };
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
winner: temporalWinner,
|
winner: temporalWinner,
|
||||||
commemorations: isAtLeast(sanctoral.rank, 'duplex-1-classis') ? [sanctoralCommemoration(sanctoral)] : [],
|
commemorations: [],
|
||||||
|
transfer: { candidate: sanctoral, direction: transferDirectionOf(sanctoral.rank) },
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'privileged-feria-major':
|
||||||
|
// Ash Wednesday, Holy Week, the Easter Octave, the Vigil of
|
||||||
|
// Christmas — never displaced, same as privileged-sunday.
|
||||||
|
if (isAtLeast(sanctoral.rank, 'duplex-majus')) {
|
||||||
|
return { winner: temporalWinner, commemorations: [sanctoralCommemoration(sanctoral)] };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
winner: temporalWinner,
|
||||||
|
commemorations: [],
|
||||||
|
transfer: { candidate: sanctoral, direction: transferDirectionOf(sanctoral.rank) },
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'ordinary-sunday':
|
case 'ordinary-sunday':
|
||||||
|
|||||||
@@ -48,12 +48,14 @@ function applyIncomingTransfer(
|
|||||||
winner: DayWinner,
|
winner: DayWinner,
|
||||||
commemorations: Commemoration[],
|
commemorations: Commemoration[],
|
||||||
): DayWinner {
|
): DayWinner {
|
||||||
if (temporalCategory === 'privileged-feria') {
|
if (temporalCategory === 'privileged-feria-major') {
|
||||||
// Deferred: a transfer landing on a privileged feria (the concrete
|
// Deferred: a transfer landing on one of these (the concrete case is
|
||||||
// case is Holy Week, right after Palm Sunday) needs its own
|
// Holy Week, right after Palm Sunday) needs its own Easter-date-keyed
|
||||||
// Easter-date-keyed lookup table, the same way the reference engine
|
// lookup table, the same way the reference engine handles it — not
|
||||||
// handles it — not modeled yet. The transfer is simply not delivered
|
// modeled yet. The transfer is simply not delivered here rather than
|
||||||
// here rather than guessed at.
|
// guessed at. The lesser privileged-feria tier doesn't need this —
|
||||||
|
// ordinary collision/transfer logic is fine there, same as any
|
||||||
|
// ordinary-sunday landing.
|
||||||
return winner;
|
return winner;
|
||||||
}
|
}
|
||||||
if (winner.kind === 'temporal') {
|
if (winner.kind === 'temporal') {
|
||||||
|
|||||||
+18
-1
@@ -70,7 +70,24 @@ export type FeastClass =
|
|||||||
// category — reconstructed from general knowledge of the pre-1955
|
// category — reconstructed from general knowledge of the pre-1955
|
||||||
// tradition, not yet verified against a primary source, so expect
|
// tradition, not yet verified against a primary source, so expect
|
||||||
// corrections.
|
// corrections.
|
||||||
export type TemporalCategory = 'ordinary-feria' | 'privileged-feria' | 'ordinary-sunday' | 'privileged-sunday';
|
// `privileged-feria` and `privileged-feria-major` are two real, distinct
|
||||||
|
// tiers (confirmed by finding a plain Duplex saint, St. Gregory the Great,
|
||||||
|
// outright winning against a Lenten Ember Saturday in the real Monastic
|
||||||
|
// 1617 engine, which the original single-tier model wrongly forbade) —
|
||||||
|
// see calendar/commemorations.ts's rules for `privileged-feria` (behaves
|
||||||
|
// like `ordinary-sunday`: Duplex+ wins outright) vs `privileged-feria-major`
|
||||||
|
// (behaves like `privileged-sunday`: never displaced at all, confirmed by
|
||||||
|
// checking St. Mark, Duplex II. classis, merely commemorated rather than
|
||||||
|
// winning within the Easter Octave). See data/calendar/temporal-
|
||||||
|
// categories.yml for exactly which days fall in which tier — some of that
|
||||||
|
// split (e.g. whether Pentecost's own Ember days share Lent's lesser tier
|
||||||
|
// or Easter's major one) is still a reconstructed guess, not verified.
|
||||||
|
export type TemporalCategory =
|
||||||
|
| 'ordinary-feria'
|
||||||
|
| 'privileged-feria'
|
||||||
|
| 'privileged-feria-major'
|
||||||
|
| 'ordinary-sunday'
|
||||||
|
| 'privileged-sunday';
|
||||||
|
|
||||||
export interface SanctoralIdentity {
|
export interface SanctoralIdentity {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -42,7 +42,12 @@ function hasFirstVespers(day: LiturgicalDay): boolean {
|
|||||||
|
|
||||||
/** Does this day keep its own Second Vespers regardless of what tomorrow is? */
|
/** Does this day keep its own Second Vespers regardless of what tomorrow is? */
|
||||||
function keepsOwnSecondVespers(day: LiturgicalDay): boolean {
|
function keepsOwnSecondVespers(day: LiturgicalDay): boolean {
|
||||||
if (day.weekday === 'sunday' || day.temporalCategory === 'privileged-feria' || isMajorFixedFeastOfTheLord(day.date)) {
|
if (
|
||||||
|
day.weekday === 'sunday' ||
|
||||||
|
day.temporalCategory === 'privileged-feria' ||
|
||||||
|
day.temporalCategory === 'privileged-feria-major' ||
|
||||||
|
isMajorFixedFeastOfTheLord(day.date)
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return day.winner.kind === 'sanctoral' && isAtLeast(day.winner.rank, 'duplex-2-classis');
|
return day.winner.kind === 'sanctoral' && isAtLeast(day.winner.rank, 'duplex-2-classis');
|
||||||
|
|||||||
@@ -15,6 +15,18 @@
|
|||||||
# easter-offsets.yml) and `fixedDates` (MM-DD) override that default for
|
# easter-offsets.yml) and `fixedDates` (MM-DD) override that default for
|
||||||
# specific privileged ferias that don't line up with a whole season.
|
# specific privileged ferias that don't line up with a whole season.
|
||||||
#
|
#
|
||||||
|
# `privileged-feria` vs `privileged-feria-major`: two real tiers, not one
|
||||||
|
# — see calendar/types.ts's TemporalCategory doc comment for how this was
|
||||||
|
# found and calendar/commemorations.ts for the rule each gets. Lent's Ember
|
||||||
|
# days are directly verified as the lesser tier (a plain Duplex wins
|
||||||
|
# outright there); Ash Wednesday, Holy Week, and the Easter Octave are the
|
||||||
|
# major tier (Easter Octave directly verified — Holy Week and Ash
|
||||||
|
# Wednesday are strong prior knowledge, not independently re-verified
|
||||||
|
# here). Where this app *doesn't* have direct evidence either way —
|
||||||
|
# Pentecost's own Ember days/Vigil/Octave, the Vigil of Christmas — it
|
||||||
|
# defaults to the major (stricter) tier as the safer guess, pending
|
||||||
|
# verification.
|
||||||
|
#
|
||||||
# Known gap: Advent Ember days (Wed/Fri/Sat after Dec 13) and September
|
# Known gap: Advent Ember days (Wed/Fri/Sat after Dec 13) and September
|
||||||
# Ember days (Wed/Fri/Sat near the Exaltation of the Cross, Sept 14) are
|
# Ember days (Wed/Fri/Sat near the Exaltation of the Cross, Sept 14) are
|
||||||
# NOT modeled here — both are fixed-calendar "nearest such-and-such a date"
|
# NOT modeled here — both are fixed-calendar "nearest such-and-such a date"
|
||||||
@@ -36,24 +48,28 @@ bySeason:
|
|||||||
pentecost: { sunday: privileged-sunday, feria: ordinary-feria }
|
pentecost: { sunday: privileged-sunday, feria: ordinary-feria }
|
||||||
trinitytide: { sunday: ordinary-sunday, feria: ordinary-feria }
|
trinitytide: { sunday: ordinary-sunday, feria: ordinary-feria }
|
||||||
# Corpus Christi/Sacred Heart are themselves "feasts of the Lord" that
|
# Corpus Christi/Sacred Heart are themselves "feasts of the Lord" that
|
||||||
# shouldn't cede to an ordinary saint — modeled here as privileged-feria
|
# shouldn't cede to an ordinary saint — modeled here as
|
||||||
# (rather than injecting a separate synthetic FeastClass) since with the
|
# privileged-feria-major (rather than injecting a separate synthetic
|
||||||
# small sanctoral calendar this app has, a real clash is unlikely and the
|
# FeastClass) since with the small sanctoral calendar this app has, a
|
||||||
# practical effect (temporal wins) is the same either way.
|
# real clash is unlikely and the practical effect (temporal wins) is the
|
||||||
corpus-christi: { sunday: ordinary-sunday, feria: privileged-feria }
|
# same either way.
|
||||||
sacred-heart: { sunday: ordinary-sunday, feria: privileged-feria }
|
corpus-christi: { sunday: ordinary-sunday, feria: privileged-feria-major }
|
||||||
|
sacred-heart: { sunday: ordinary-sunday, feria: privileged-feria-major }
|
||||||
|
|
||||||
offsets:
|
offsets:
|
||||||
-46: privileged-feria # Ash Wednesday
|
-46: privileged-feria-major # Ash Wednesday
|
||||||
-39: privileged-feria # Lent Ember Wednesday (Wed after 1st Sunday of Lent)
|
-39: privileged-feria # Lent Ember Wednesday (Wed after 1st Sunday of Lent) — verified: Duplex wins outright here
|
||||||
-37: privileged-feria # Lent Ember Friday
|
-37: privileged-feria # Lent Ember Friday
|
||||||
-36: privileged-feria # Lent Ember Saturday
|
-36: privileged-feria # Lent Ember Saturday — verified: Duplex wins outright here (St. Gregory the Great)
|
||||||
48: privileged-feria # Vigil of Pentecost
|
48: privileged-feria-major # Vigil of Pentecost — unverified, defaulted to the stricter tier
|
||||||
|
|
||||||
offsetRanges:
|
offsetRanges:
|
||||||
- { fromOffset: -6, toOffset: -1, category: privileged-feria } # ferias of Holy Week
|
- { fromOffset: -6, toOffset: -1, category: privileged-feria-major } # ferias of Holy Week
|
||||||
- { fromOffset: 1, toOffset: 6, category: privileged-feria } # Easter octave
|
- { fromOffset: 1, toOffset: 6, category: privileged-feria-major } # Easter octave — verified: even Duplex II. classis (St. Mark) only gets commemorated, never wins
|
||||||
- { fromOffset: 50, toOffset: 55, category: privileged-feria } # Pentecost octave (also covers Pentecost's own Ember days)
|
# Pentecost octave (also covers Pentecost's own Ember days) — unverified,
|
||||||
|
# defaulted to the stricter tier; Lent's Ember days are confirmed
|
||||||
|
# lesser-tier, but that hasn't been checked for Pentecost's specifically.
|
||||||
|
- { fromOffset: 50, toOffset: 55, category: privileged-feria-major }
|
||||||
|
|
||||||
fixedDates:
|
fixedDates:
|
||||||
"12-24": privileged-feria # Vigil of Christmas
|
"12-24": privileged-feria-major # Vigil of Christmas — unverified, defaulted to the stricter tier
|
||||||
|
|||||||
@@ -42,13 +42,43 @@ describe('decideOccurrence — ordinary-feria', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('decideOccurrence — privileged-feria', () => {
|
describe('decideOccurrence — privileged-feria', () => {
|
||||||
it('yields to nothing short of the top class', () => {
|
it('behaves exactly like an ordinary Sunday — duplex or higher wins outright, the feria is commemorated in return', () => {
|
||||||
const majus = decideOccurrence('privileged-feria', 'id', saint('duplex-2-classis'));
|
// Verified: St. Gregory the Great, plain Duplex, outright wins against
|
||||||
expect(majus.winner).toEqual({ kind: 'temporal', id: 'id' });
|
// a Lenten Ember Saturday in the real Monastic 1617 engine.
|
||||||
expect(majus.commemorations).toEqual([]);
|
const result = decideOccurrence('privileged-feria', 'id', saint('duplex'));
|
||||||
|
expect(result.winner).toEqual({ kind: 'sanctoral', id: 'x', name: 'St. Ereden', rank: 'duplex' });
|
||||||
|
expect(result.commemorations).toEqual([{ kind: 'temporal', id: 'id' }]);
|
||||||
|
});
|
||||||
|
|
||||||
const top = decideOccurrence('privileged-feria', 'id', saint('duplex-1-classis'));
|
it('simplex stays and is commemorated, not transferred', () => {
|
||||||
expect(top.commemorations).toEqual([{ kind: 'sanctoral', id: 'x', name: 'St. Ereden', rank: 'duplex-1-classis' }]);
|
const result = decideOccurrence('privileged-feria', 'id', saint('simplex'));
|
||||||
|
expect(result.winner).toEqual({ kind: 'temporal', id: 'id' });
|
||||||
|
expect(result.commemorations).toEqual([{ kind: 'sanctoral', id: 'x', name: 'St. Ereden', rank: 'simplex' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('semiduplex/vigil get nothing here — transfer instead', () => {
|
||||||
|
const result = decideOccurrence('privileged-feria', 'id', saint('semiduplex'));
|
||||||
|
expect(result.commemorations).toEqual([]);
|
||||||
|
expect(result.transfer).toEqual({ candidate: saint('semiduplex'), direction: 'forward' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('decideOccurrence — privileged-feria-major', () => {
|
||||||
|
it('is never displaced, same as privileged-sunday — duplex-majus and up are commemorated', () => {
|
||||||
|
// Verified: St. Mark, Duplex II. classis, only ever commemorated —
|
||||||
|
// never wins outright — within the Easter Octave.
|
||||||
|
for (const rank of ['duplex-majus', 'duplex-2-classis', 'duplex-1-classis'] as const) {
|
||||||
|
const result = decideOccurrence('privileged-feria-major', 'id', saint(rank));
|
||||||
|
expect(result.winner).toEqual({ kind: 'temporal', id: 'id' });
|
||||||
|
expect(result.commemorations).toEqual([{ kind: 'sanctoral', id: 'x', name: 'St. Ereden', rank }]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('yields nothing short of duplex-majus — transfers instead', () => {
|
||||||
|
const result = decideOccurrence('privileged-feria-major', 'id', saint('duplex'));
|
||||||
|
expect(result.winner).toEqual({ kind: 'temporal', id: 'id' });
|
||||||
|
expect(result.commemorations).toEqual([]);
|
||||||
|
expect(result.transfer).toEqual({ candidate: saint('duplex'), direction: 'forward' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user