summaryrefslogtreecommitdiff
path: root/lib/kernel
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-08-12 02:37:03 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-08-12 02:37:03 +0200
commit4235a6aa18b815c5457a7eb97fd97eb4919dfd4b (patch)
tree053625ce1e05c550feaf85a533a67456af8a13c1 /lib/kernel
parent51f8a25da4f0f939e0a77ce89b5c4b769c6bfe48 (diff)
downloadcolitur-4235a6aa18b815c5457a7eb97fd97eb4919dfd4b.tar.gz
colitur-4235a6aa18b815c5457a7eb97fd97eb4919dfd4b.zip
kernel(validate): fold in Plan 2's carried guards
Three carried items from Plan 2's parked rulings, closed: 1. Slug uniqueness moves from a 200-sample QCheck property scoped to one rite (test_temporal_ef.ml) into Validate's own "slugs" check, so every consumer gets it. The resumed-Sunday exemption that property carried is dropped, not weakened elsewhere: Plan 2 verified zero duplicate slugs domain-wide (all 8 416 years), and by construction a resumed Sunday only ever backfills a week number Septuagesima cut short that same liturgical year, so it can never repeat a number that year's own January Sundays already used. The now-redundant property and its is_resumable_sunday_slug helper are removed from test_temporal_ef.ml; test_validate.ml's own domain-wide property covers the same ground for every consumer. 2. The anchors-erosion guard (Plan 2: deleting entries from a rite's anchors list left the whole suite green) is implemented, but not in Validate. Which of a rite's named days are Easter-derived is knowledge only the rite's own `named` function has; Rite.t deliberately exposes only `temporal` and `anchors`, never `named`, so a rite-agnostic Validate has no ground truth to check anchors' completeness against. Hardcoding an Easter offset, or even Easter itself, would smuggle Western/Gregorian-specific knowledge into code meant to also serve a future Julian-reckoning rite; rediscovering "named-ness" structurally from `temporal` alone is unsound for EF, since most ordinary Sunday/feria slugs from Septuagesima onward are also constant-offset-from-Easter by construction. The guard is therefore EF-specific and lives in test_temporal_ef.ml, discovering the Easter-derived slug set mechanically (scanning a window around Easter and keeping whatever `named` answers Some for) rather than hand-copying either named's or anchors' own offset list, then asserting completeness against the real anchors for the domain's Easter extremes (1598, 1666) plus an ordinary year. A negative fixture proves the guard has teeth, matching Plan 2's exact regression (anchors missing "ef-ascension" reports it, and only it, as missing). 3. test_validate.ml's extreme_years comment claimed 1818/2038; verified against Computus.gregorian_easter directly, the domain's actual Easter extremes (1583..2500) are 1598/1666. Corrected. Verification: the full 1583..9999 domain sweep (233 tests via dune test's 200-sample default, plus a manual full sweep) reports exactly one failure -- the known, already-pinned year-9999 season-truncation case -- and zero occurrences of the new "slugs" check anywhere in the domain. Deleting "ef-ascension" from the real anchors list (reproducing Plan 2's regression directly) is caught immediately by the new EF test and, confirmed empirically, invisible to Validate's own full property sweep -- direct evidence for why item 2 cannot live in Validate.
Diffstat (limited to 'lib/kernel')
-rw-r--r--lib/kernel/validate.ml49
-rw-r--r--lib/kernel/validate.mli7
2 files changed, 44 insertions, 12 deletions
diff --git a/lib/kernel/validate.ml b/lib/kernel/validate.ml
index d8de31b..cc8bdce 100644
--- a/lib/kernel/validate.ml
+++ b/lib/kernel/validate.ml
@@ -20,6 +20,17 @@ let has_duplicate strings =
let rec go = function a :: (b :: _ as rest) -> a = b || go rest | _ -> false in
go sorted
+(* Like [has_duplicate], but names the offender(s) instead of only reporting
+ that one exists -- the ["slugs"] check below wants a useful failure
+ detail, not just a bool. *)
+let duplicates strings =
+ let sorted = List.sort String.compare strings in
+ let rec go acc = function
+ | a :: (b :: _ as rest) -> go (if a = b then a :: acc else acc) rest
+ | _ -> acc
+ in
+ List.sort_uniq String.compare (go [] sorted)
+
(* Task 12's "unconverged" check has no structural signal to key off --
Calendar's placement pass records its round-guard reason as a plain
string in [Liturgical_day.omitted] (calendar.ml's own [unconverged_reason],
@@ -85,18 +96,16 @@ let run (rite : ('s, 'r) Rite.t) (layer : 'r Layer.t) ~year =
(* Weekday agreement. *)
if t.Temporal.weekday <> Date.weekday date then
fail date "weekday" "temporal weekday disagrees with Date.weekday";
- (* Slug: three properties, none checked here, all delivered
- elsewhere. Well-formedness needs no check: [Slug.t] is a private
- string validated on every construction path ([of_string],
- [of_string_exn], [t_of_sexp]), and [to_string] is the identity,
- so round-tripping an existing [Slug.t] can never fail -- a check
- here would be structurally incapable of firing, which is worse
- than no check, since it would look like coverage that isn't
- there. Uniqueness *per date* needs no check either: [temporal]
- returns exactly one office by construction. Uniqueness *across
- the year* is deliberately NOT asserted -- a resumed Sunday
- reuses an earlier Epiphany key on purpose, so the check would be
- false. *)
+ (* Slug: three properties. Well-formedness needs no check: [Slug.t]
+ is a private string validated on every construction path
+ ([of_string], [of_string_exn], [t_of_sexp]), and [to_string] is
+ the identity, so round-tripping an existing [Slug.t] can never
+ fail -- a check here would be structurally incapable of firing,
+ which is worse than no check, since it would look like coverage
+ that isn't there. Uniqueness *per date* needs no check either:
+ [temporal] returns exactly one office by construction.
+ Uniqueness *across the year* IS asserted, below, once the whole
+ walk is in hand -- see the ["slugs"] check after this loop. *)
(* Vocabulary closure. *)
if not (List.exists (fun r -> vocab.Vocab.rank_to_string r
= vocab.Vocab.rank_to_string cel.Celebration.rank)
@@ -114,6 +123,22 @@ let run (rite : ('s, 'r) Rite.t) (layer : 'r Layer.t) ~year =
| None -> fail date "determinism" "a second call to temporal raised where the first succeeded"))
days;
let observed = List.rev !observed in
+ (* Slug uniqueness across the year (Plan 2 carried item 4): moved into
+ [Validate] itself so every consumer gets it, not only a 200-sample
+ QCheck property scoped to one rite. Asserted OUTRIGHT, no exemption:
+ Plan 2 verified zero duplicate slugs domain-wide, across all 8 416
+ years, for the EF rite's own resumed-Sunday mechanism -- the exemption
+ the test property used to carry protected nothing real, because a
+ resumed Sunday only ever backfills a week number Septuagesima cut short
+ that same liturgical year (so it was never actually used that year to
+ begin with), never repeats one the year's own January Sundays already
+ used. If a future rite genuinely needs an exemption, it can supply one
+ then -- not speculatively here. *)
+ (match duplicates (List.map (fun (_, t) -> Slug.to_string t.Temporal.office.Celebration.slug) observed) with
+ | [] -> ()
+ | dups ->
+ fail start "slugs"
+ (Printf.sprintf "slug(s) sighted on more than one date this year: %s" (String.concat ", " dups)));
(* Season contiguity and completeness: the run-length-compressed sequence must
equal the rite's own [season_runs] exactly, in canonical order. This is
NOT necessarily [vocab.seasons] -- most rites have each season in one
diff --git a/lib/kernel/validate.mli b/lib/kernel/validate.mli
index 511c78f..5e55fc5 100644
--- a/lib/kernel/validate.mli
+++ b/lib/kernel/validate.mli
@@ -17,6 +17,13 @@ val failure_to_string : failure -> string
straddles two civil years, and checks only the pairs whose date actually
falls within the year walked.
+ ["slugs"]: no two dates within the walked liturgical year may carry the
+ same office slug (Plan 2 carried item 4). Asserted outright, with no
+ exemption for the resumed-Sunday reuse a slug's own name might suggest:
+ a resumed Sunday only ever backfills a week number Septuagesima cut
+ short that same year, so by construction it never repeats a number that
+ year's own January Sundays actually used.
+
The season check compares the run-length-compressed season sequence
against [rite.Rite.season_runs], not [rite.Rite.vocab.seasons]: a rite may
have one season appear in two separate runs (the modern form's Ordinary