diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 02:37:03 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 02:37:03 +0200 |
| commit | 4235a6aa18b815c5457a7eb97fd97eb4919dfd4b (patch) | |
| tree | 053625ce1e05c550feaf85a533a67456af8a13c1 /lib | |
| parent | 51f8a25da4f0f939e0a77ce89b5c4b769c6bfe48 (diff) | |
| download | colitur-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')
| -rw-r--r-- | lib/kernel/validate.ml | 49 | ||||
| -rw-r--r-- | lib/kernel/validate.mli | 7 |
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 |
