aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-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