| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
test_validate.ml's own prop_invariants samples 200 of 8 416 years
(2.4% of the domain) on a random seed -- QCheck.Test.make draws a fresh
one from the environment each run when none is given, and two consecutive
runs of this suite were observed using different seeds. CLAUDE.md's
standing claim that Validate is "clean across all 8 416 years --
exhaustive, not sampled" was true whenever it was last actually re-run in
full, but pinned by no committed artifact, and a year-specific regression
would show up in this suite only intermittently.
Added test_exhaustive_domain_sweep: every year 1583..9999 through
Validate.run, not a sample, with 9999 handled via the same documented
truncation test_year_9999_does_not_raise already pins (the domain ceiling
means the season-run check legitimately, and only, fires there).
Tagged `Slow, but NOT wired through Alcotest's -q/--quick-tests flag at
the dune level: that filtering is all-or-nothing per speed level, and
this codebase already tags six OTHER cases `Slow -- the two pre-existing
exhaustive Computus checks (fast, sub-second) and, found while
implementing this item, every QCheck property in the suite (test_date.ml
x3, test_overlay.ml, test_temporal_ef.ml, prop_invariants itself), since
QCheck_alcotest.to_alcotest defaults ~speed_level to `Slow when not given
explicitly and no call site in this codebase overrides it. Wiring
`(action (run %{test} -q))` into test/dune was tried and reverted: it
dropped the default `dune test`'s reported count from 260 to 251,
silently excluding prop_invariants itself -- the project's own
"confidence-to-9999" property -- along with five others, a far bigger
regression than the one test this item asks to add.
Instead the new test gates its own body on an environment variable,
COLITUR_EXHAUSTIVE_SWEEP, and calls Alcotest.skip when unset -- reported
honestly as SKIP, not a vacuous pass, and not counted toward "tests run".
Default `dune test` is therefore unaffected (259 tests run, same as
before this commit, 1 skipped) and stays at ~2.6s. Run the real sweep
with:
COLITUR_EXHAUSTIVE_SWEEP=1 dune test --force
Measured runtime: ~49-53s (three separate invocations, including dune's
own build/dependency-check overhead), against the ~35-45s estimate --
same order of magnitude, reported as measured rather than adjusted to
match the estimate.
No behaviour change to any existing test. Verified byte-identical
`colitur day` output across 1583, 1900, 1902, 2008, 2011, 2026, 2038,
9999, both with and without COLITUR_EXHAUSTIVE_SWEEP set.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Widen Validate.run to take the rite's sanctoral layer alongside the rite
itself (Calendar.year needs both), and add five checks over the fully
resolved liturgical year, on top of the existing temporal-only pass:
- observed: a day's observed celebration never also appears among that
same day's own commemorations/omissions.
- lost: no sanctoral entry is silently dropped. Per slug, the number of
times it is actually sighted (observed + commemorations + omitted,
summed over the year) must never fall below the number of times its
own Date_spec resolves within the year's span -- also fires if
resolving the year raises at all, the most total form of loss.
- duplicated: the same per-slug count must never exceed the number of
Date_spec resolutions either. Deliberately NOT "no slug appears
twice": a fixed date can legitimately resolve twice in the ~20% of
liturgical years whose 371-day span reaches it on both ends (30
November/St Andrew is the worked example in validate.mli).
- unconverged: no day's omitted reason indicates Calendar's placement
pass hit its round guard before reaching a fixed point.
- admission: the rite's own rules.admit is a fixed point on what it
already admitted -- the rite-agnostic form of "the admission limit
was not exceeded" available without embedding a rite's own numeric
caps (RG 111's, for EF) into kernel code.
Each check has a dedicated negative fixture in the synthetic rite
(test_validate.ml), hand-traced against Calendar's actual resolution
mechanics before writing the assertion, and verified to fail for the
right reason against the code before this change. One pair
(unconverged/duplicated) is not fully independent: hitting the round
guard genuinely also trips duplicated, a real consequence of Calendar's
own accounting once a candidate is simultaneously sighted at its
permanent natural date and wherever the last placement round left it --
documented in guard_rules's own comment, not papered over.
test_validate.ml's ef_rite/run now use the real Rite_ef.context and the
real bootstrapped data/ef layer (Precedence_ef and the sanctoral
bootstrap did not exist when this scaffolding was first written) rather
than the earlier placeholder rules. Validate is clean across the whole
1583..9999 domain against real EF data except the one already-documented
year-9999 truncation case (test_year_9999_does_not_raise).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Calendar.year now runs a placement pass after resolving every day: each
deferred candidate (RG 95's I-class-only right of translation, via
Precedence's Transfer disposition) is placed on the next day the rite's
new Rite.t.transfer_target names as admissible, transferred_in/out are
set on the two ends of the move, and the whole year is re-resolved to a
fixed point, bounded by a hard max_transfer_rounds = 64 guard.
transfer_target is rite-supplied rather than a generic search Calendar
drives itself: RG 96's 'not I or II class' is not derivable from band or
disposition alone (RG 91's own table lets a universal I-class feast
outrank an ordinary Sunday in a raw contest, yet RG 96 forbids landing a
translation there regardless), and the search's starting point is
rite-specific too (the Annunciation exception). It takes an occupant
callback exposing what Calendar currently resolves as observed on any
date, so the rite never has to re-implement occurrence resolution.
Two correctness properties drove most of the design:
- A candidate's permanent natural loss at its own origin (the layer entry
never moves) is rediscovered every round; left unfiltered this
oscillates a placed candidate between two dates forever, since its own
rank makes it look 'occupied' to a fresh search from its origin. Both
the round loop's gather and the final per-day omitted accounting filter
this out, keeping only sightings that are either brand new or losing at
a candidate's *current* target (a fresh RG 97-98 bump).
- RG 97-98's sort has to actually decide something, not just happen to
agree with Precedence.resolve's own tie-break next round: a
claimed-this-round overlay lets earlier-processed candidates in one
round block later ones in the same pass, so two coinciding I-class
feasts land on consecutive admissible days in the one round they
collide, in band order.
Also folds in Task 5's review finding: year_bounds clamps y to [1582,
9999] once, up front, rather than guarding start and stop independently
(each guard only ever covered one of the two rite.year_start calls,
leaving year 999 and year 100000 each able to call it out of domain
through the other branch).
|
| |
|
|
|
|
|
|
|
|
|
| |
Validate took four loose arguments that had to come from the same rite with
nothing enforcing it, and Calendar is about to add more. Bundling makes a
mismatched assembly unrepresentable through the normal path.
season_runs replaces the hardcoded assumption that every season occupies exactly
one unbroken run. That holds for the 1962 rite but is false for the modern
form's Ordinary Time, which is one season in two runs -- as written the check
would have reported a false failure every year for the second rite.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Validate.run ~year:9999 raised (year_start (year + 1) asked year_start
for civil year 10000, out of the kernel's 1583..9999 domain), even
though 9999 is itself in range and kernel computation must never raise
on in-range input; ~year:9998 already returned zero failures. run now
clamps its scan to 31 December 9999 instead of computing year_start
(year + 1) when year is the domain maximum, and validates the
resulting truncated final liturgical year rather than not being able
to run it at all.
The design spec's validation §5 lists eight checks; only five were
implemented (coverage, seasons, weeks, weekday, closure). The two
missing were a real gap, not just a documentation slip:
- §5.7 anchor agreement. All of an EF year's Easter-derived and fixed
named days were pinned only by point assertions for 2026. run now
takes an ~anchors:(int -> (string * Date.t) list) parameter -- the
rite's own independent restatement of those dates, paired with the
slug each should carry, not derived from temporal itself -- and
checks that temporal agrees on every one of them. Temporal_ef.anchors
supplies EF's list. Kept rite-agnostic: the anchor list comes from
the rite argument, not the kernel.
- §5.8 determinism. run now calls temporal a second time for every
date and checks the result is structurally equal to the first.
Also, finding 8: the rank/season closure checks compare vocab entries
via their _to_string images, which is only sound if those images are
injective. run now checks List.map rank_to_string ranks and
List.map season_to_string seasons for duplicates up front and reports
a "vocab" failure if either collapses two distinct values to the same
string, rather than relying on that injectivity unasserted.
Test-quality fixes to the existing synthetic fixture, found while
adding coverage for the above: the fixture's own comment claimed its
mutation target (2026-03-15) was "not a Sunday" and "sits safely
mid-run" -- it is a Sunday, which made the coverage/week mutations
cascade further than documented even though the assertions still
target specific check labels. Moved to a genuine mid-week day
(2026-03-17) and the comment corrected. extreme_years's own test
required only "found at least one" of the two Easter-extreme years;
tightened to require both, since both genuinely exist in 1583..2500.
Covering tests: test_year_9999_does_not_raise (would error under the
old code; the fix is pinned by calling run 9999 directly with no try,
plus asserting the truncated year is reported via an ordinary
"seasons" failure, not silently or via coverage); anchor-clean and
anchor-fires cases on the synthetic rite; a determinism-fires case
using a target date whose temporal alternates what it returns across
successive calls; two vocab-injectivity-fires cases (collapsed rank
strings, collapsed season strings).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Task 14 review, findings 1 and 2.
Finding 1: the only tests against Validate exercised the clean path against
real EF data, so the evidence that each check can actually fire lived in a
scratch mutation probe that was never committed. A future edit that quietly
weakened a check would leave the suite green, since a weaker check only
makes more inputs pass. Added a small synthetic two-season, two-rank rite
fixture in test_validate.ml -- not EF -- letting each test violate exactly
one invariant directly: a temporal that raises (coverage), a season that
recurs (seasons), a week that decreases mid-run (week), a weekday that
disagrees with Date.weekday (weekday), a rank absent from the declared vocab
(rank), and a colour outside Colour.all (colour, via a same-representation
Obj.magic value, safe here because the check compares by structural equality
rather than pattern match). A clean-baseline test confirms the fixture itself
reports zero failures before any mutation is applied.
Each new test was verified non-vacuous by temporarily weakening its
corresponding check in validate.ml, confirming the matching test fails, then
reverting -- the same trap one level up, checked explicitly rather than
assumed.
Finding 2: removed the slug well-formedness check. Slug.t is a private string
validated on every construction path, and to_string is the identity, so
round-tripping an existing Slug.t can never fail -- the check was structurally
incapable of firing. Folded the explanation into the comment block that
already covers why slug uniqueness isn't checked, since it's the same kind of
fact: a property the type system delivers, not one Validate needs to assert.
The colour check stays: unlike slug, Colour.all is a hand-maintained list
that can drift from the type, so it is only practically (not structurally)
tautological, the same class as the rank closure check.
|
|
|
Coverage, season contiguity and completeness, Sunday-aligned week numbering,
slug well-formedness, weekday agreement and vocabulary closure.
Checks run over a liturgical year rather than a civil one, since Christmastide
straddles January and would otherwise appear to recur.
Run against EF temporal for landmark years, both Easter extremes, and 200
random years across 1583..9998 -- the property layer is how confidence reaches
past the oracle horizon.
|