summaryrefslogtreecommitdiff
path: root/lib/kernel/rite.mli
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-12 11:38:35 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-12 11:38:35 +0200
commite19adb9c7ea369cafe76e7eff50e9248a4a954d0 (patch)
tree57824e121606286ebdf8fff68439986dd3300eae /lib/kernel/rite.mli
parent0506388da160a15ceddb0cea1a697d737103d5c4 (diff)
parent36df2bd0af9a555a09334e14b0d89118be1dbbc0 (diff)
downloadcolitur-e19adb9c7ea369cafe76e7eff50e9248a4a954d0.tar.gz
colitur-e19adb9c7ea369cafe76e7eff50e9248a4a954d0.zip
Merge branch 'ef-plan3': Plan 3, the EF resolution engine
Adds the rite-parameterised Precedence resolver, Liturgical_day, Rite, and Calendar (year as the primitive, because transfers need whole-year knowledge), the full EF precedence ruleset (RG 91's 28-entry table, occurrence RG 92-95, commemorations RG 108-111, transfers RG 96-98), 322 bootstrapped sanctoral entries, colitur day <year>, and validation layers 3-5. Layer 3 diffs 16801 days against lectio; layer 4 diffs 730 days against missalemeum; layer 5 pins ~30 dates on the known-tricky years. Both comparison layers carry cited allow-lists that name the governing RG paragraph and which engine is right. The oracle layer earned its place immediately: Holy Thursday was violet in colitur and lectio alike, because colitur's data was bootstrapped from lectio and both carried the same error. Only an independent source could see it. RG 128(b) and RG 122 name it white.
Diffstat (limited to 'lib/kernel/rite.mli')
-rw-r--r--lib/kernel/rite.mli62
1 files changed, 62 insertions, 0 deletions
diff --git a/lib/kernel/rite.mli b/lib/kernel/rite.mli
new file mode 100644
index 0000000..ffe9471
--- /dev/null
+++ b/lib/kernel/rite.mli
@@ -0,0 +1,62 @@
+(** Everything a rite supplies, bundled. Passing these as loose arguments let a
+ caller pair one rite's vocab with another's temporal; bundling makes that
+ unrepresentable through the normal path. Carries functions, so it has no
+ sexp form. *)
+type ('s, 'r) t = {
+ id : string;
+ vocab : ('s, 'r) Vocab.t;
+ year_start : int -> Date.t;
+ (** first day of the liturgical year opening in civil year y *)
+ temporal : Date.t -> ('s, 'r) Temporal.t;
+ anchors : int -> (string * Date.t) list;
+ (** Easter-derived days: (expected slug, date) *)
+ rules : ('s, 'r) Precedence.rules;
+ season_runs : 's list;
+ (** the expected run-length-compressed season sequence over one liturgical
+ year. NOT necessarily [vocab.seasons]: a rite may have one season
+ appear in two separate runs (the modern form's Ordinary Time does). *)
+ transfer_target :
+ 'r Precedence.candidate -> Date.t -> (Date.t -> 'r Celebration.t) -> Date.t;
+ (** RG 96: where an impeded I-class feast goes. Given the deferred
+ candidate, the date it was impeded on, and [occupant] -- a callback
+ exposing what {!Calendar} currently resolves as observed on any
+ given date -- returns the date to place it on.
+
+ Deliberately one rite-supplied function, not a generic search Calendar
+ drives itself: "not I or II class" is not derivable from [band] or
+ [disposition] alone. RG 91's own table would let a universal I-class
+ feast (entry 11) numerically outrank an ordinary Sunday (entry 15,
+ II class) in a raw occurrence contest -- entry 11 comes before entry
+ 15, and lower wins -- so testing "would the translated feast win
+ here" is not the same question as "is this day free to receive a
+ translation": RG 96 forbids landing on the Sunday regardless of
+ which one would structurally win. Only the rite knows which of its
+ own ranks are exempt from translation onto them. The rite also
+ owns the search's starting point, because RG 96's exception is
+ rite-specific too: the Annunciation does not search forward from
+ its own impeded date at all, it goes straight to the Monday after
+ Low Sunday (searching onward from there only if that day is itself
+ blocked). [occupant] is supplied rather than a raw layer/temporal
+ pair so the rite never has to re-implement occurrence resolution
+ just to answer "what sits here".
+
+ OBLIGATIONS (not enforced by the type, and {!Calendar}'s own
+ termination argument depends on both): the result must be
+ {b strictly later} than the [Date.t] argument (the date the
+ candidate was impeded on) -- {!Calendar}'s placement pass treats
+ [target = origin] or [target < origin] as a legitimate placement,
+ not an error, so a rite whose search can stand still or go
+ backward would silently loop candidates in place or resurrect an
+ already-superseded occupant rather than failing loudly. The call
+ must also {b terminate} on its own: {!Calendar}'s round guard
+ (calendar.ml's [max_transfer_rounds]) bounds how many ROUNDS the
+ whole-year placement pass takes, which is a distinct, outer thing
+ from whatever internal search a single call to this function runs
+ -- an implementation that walks forward day by day looking for an
+ admissible date, without its own bound, can hang the caller
+ outright on a rite/data shape it does not handle, never reaching
+ the round guard at all. See rite_ef/precedence_ef.ml's
+ [transfer_target] for a concrete termination argument (a
+ structural step bound, not an appeal to the real calendar's own
+ structure). *)
+}