aboutsummaryrefslogtreecommitdiff
path: root/lib/kernel
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-11 11:59:43 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-11 11:59:43 +0200
commitabade24943f2f77c058b5ff768093b8babc9a8c3 (patch)
tree33285f68860d21144bdad8388fabaf073b4771cb /lib/kernel
parent8b6c0036cb6c7f0e48a9370f53be25fd2bb243f4 (diff)
downloadcolitur-abade24943f2f77c058b5ff768093b8babc9a8c3.tar.gz
colitur-abade24943f2f77c058b5ff768093b8babc9a8c3.zip
kernel: Vocab, Celebration and Temporal parametric types
Rite specificity is carried by type parameters plus a vocab record of operations rather than by functors: the same guarantee that a rite cannot name another rite's season, without threading module plumbing through every kernel module. Celebration takes only the rank parameter, since it has no season field and OCaml rejects a type variable that appears in no field.
Diffstat (limited to 'lib/kernel')
-rw-r--r--lib/kernel/celebration.ml20
-rw-r--r--lib/kernel/celebration.mli16
-rw-r--r--lib/kernel/temporal.ml26
-rw-r--r--lib/kernel/temporal.mli24
-rw-r--r--lib/kernel/vocab.ml19
-rw-r--r--lib/kernel/vocab.mli19
6 files changed, 124 insertions, 0 deletions
diff --git a/lib/kernel/celebration.ml b/lib/kernel/celebration.ml
new file mode 100644
index 0000000..3f7a577
--- /dev/null
+++ b/lib/kernel/celebration.ml
@@ -0,0 +1,20 @@
+open Sexplib0.Sexp_conv
+
+(* A celebration as computed or as loaded from a layer.
+
+ One type parameter, not two: the record has no season field, and OCaml
+ rejects a type variable that appears in no field. *)
+type 'r t = {
+ slug : Slug.t;
+ names : Names.t;
+ rank : 'r;
+ colour : Colour.t;
+ subject : Subject.t;
+ citations : Citation.t list;
+ layer : string; (** provenance: "temporal", a layer id, or an overlay id *)
+}
+[@@deriving sexp]
+
+let make ~slug ?(names = Names.empty) ~rank ~colour ?(subject = Subject.Temporal)
+ ?(citations = []) ~layer () =
+ { slug; names; rank; colour; subject; citations; layer }
diff --git a/lib/kernel/celebration.mli b/lib/kernel/celebration.mli
new file mode 100644
index 0000000..1c84d35
--- /dev/null
+++ b/lib/kernel/celebration.mli
@@ -0,0 +1,16 @@
+(** A celebration. Parameterised by the rite's rank type only. *)
+type 'r t = {
+ slug : Slug.t;
+ names : Names.t;
+ rank : 'r;
+ colour : Colour.t;
+ subject : Subject.t;
+ citations : Citation.t list;
+ layer : string;
+}
+[@@deriving sexp]
+
+(** [subject] defaults to [Subject.Temporal], [names] to empty, [citations] to []. *)
+val make :
+ slug:Slug.t -> ?names:Names.t -> rank:'r -> colour:Colour.t ->
+ ?subject:Subject.t -> ?citations:Citation.t list -> layer:string -> unit -> 'r t
diff --git a/lib/kernel/temporal.ml b/lib/kernel/temporal.ml
new file mode 100644
index 0000000..a34eeb0
--- /dev/null
+++ b/lib/kernel/temporal.ml
@@ -0,0 +1,26 @@
+open Sexplib0.Sexp_conv
+
+(* The temporal identity of a date: where in the liturgical year it sits, and
+ the temporal cycle's own office for it. *)
+type ('s, 'r) t = {
+ season : 's;
+ week : int option; (** [None] for named days outside a numbered week *)
+ weekday : Date.weekday;
+ office : 'r Celebration.t;
+}
+[@@deriving sexp]
+
+module type RITE = sig
+ val id : string
+
+ type season
+ type rank
+
+ val vocab : (season, rank) Vocab.t
+
+ (** First day of the liturgical year opening in civil year [y]. *)
+ val year_start : int -> Date.t
+
+ (** Total over 1583..9999: every date yields exactly one temporal identity. *)
+ val temporal : Date.t -> (season, rank) t
+end
diff --git a/lib/kernel/temporal.mli b/lib/kernel/temporal.mli
new file mode 100644
index 0000000..055f9aa
--- /dev/null
+++ b/lib/kernel/temporal.mli
@@ -0,0 +1,24 @@
+(** The temporal identity of a date: where in the liturgical year it sits, and
+ the temporal cycle's own office for it. *)
+type ('s, 'r) t = {
+ season : 's;
+ week : int option; (** [None] for named days outside a numbered week *)
+ weekday : Date.weekday;
+ office : 'r Celebration.t;
+}
+[@@deriving sexp]
+
+module type RITE = sig
+ val id : string
+
+ type season
+ type rank
+
+ val vocab : (season, rank) Vocab.t
+
+ (** First day of the liturgical year opening in civil year [y]. *)
+ val year_start : int -> Date.t
+
+ (** Total over 1583..9999: every date yields exactly one temporal identity. *)
+ val temporal : Date.t -> (season, rank) t
+end
diff --git a/lib/kernel/vocab.ml b/lib/kernel/vocab.ml
new file mode 100644
index 0000000..78729bb
--- /dev/null
+++ b/lib/kernel/vocab.ml
@@ -0,0 +1,19 @@
+(* A rite's season and rank vocabulary, as a record of operations. Carries
+ functions, so it has no sexp form.
+
+ The kernel is parameterised by type variables plus this record rather than by
+ functors (spec §2.3): the safety that matters -- EF code cannot name an OF
+ season -- is the same either way, and ppx_sexp_conv derives converters for
+ parametric types natively. *)
+type ('s, 'r) t = {
+ seasons : 's list;
+ (** canonical liturgical-year order; Validate's contiguity check reads this *)
+ season_to_string : 's -> string;
+ season_of_string : string -> 's option;
+ ranks : 'r list;
+ (** documentation order, highest first. Plan 2 uses it only for the
+ closure check -- it is not a precedence relation until Plan 3
+ defines one. *)
+ rank_to_string : 'r -> string;
+ rank_of_string : string -> 'r option;
+}
diff --git a/lib/kernel/vocab.mli b/lib/kernel/vocab.mli
new file mode 100644
index 0000000..78729bb
--- /dev/null
+++ b/lib/kernel/vocab.mli
@@ -0,0 +1,19 @@
+(* A rite's season and rank vocabulary, as a record of operations. Carries
+ functions, so it has no sexp form.
+
+ The kernel is parameterised by type variables plus this record rather than by
+ functors (spec §2.3): the safety that matters -- EF code cannot name an OF
+ season -- is the same either way, and ppx_sexp_conv derives converters for
+ parametric types natively. *)
+type ('s, 'r) t = {
+ seasons : 's list;
+ (** canonical liturgical-year order; Validate's contiguity check reads this *)
+ season_to_string : 's -> string;
+ season_of_string : string -> 's option;
+ ranks : 'r list;
+ (** documentation order, highest first. Plan 2 uses it only for the
+ closure check -- it is not a precedence relation until Plan 3
+ defines one. *)
+ rank_to_string : 'r -> string;
+ rank_of_string : string -> 'r option;
+}