| Commit message (Collapse) | Author | Age | Files | Lines |
| ... | |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The register was transcribed faithfully but was itself wrong: RG 96's
Attamen (a), primary-source-verified from the scans and now corrected
in the register, reads 'festum Annuntiationis B. Mariae Virg., quando
est transferendum post Pascha, transfertur ... in feriam II post
dominicam in albis' -- the Monday-after-Low-Sunday seat applies ONLY
'quando est transferendum post Pascha', when the feast is to be
transferred PAST EASTER. The unconditional transcription made the
exception fire on every impeded Annunciation regardless of cause.
transfer_target now computes the general RG 96 target first, for every
candidate, and overrides to the Monday after Low Sunday only when that
general target itself falls after Easter Sunday -- testing the rubric's
own condition directly rather than re-deriving a date-proximity rule
from first principles.
Confirmed against three real years the review named: 2007, 2012 and
2057 all previously sent the Annunciation to Easter + 8 (16 April,
16 April, 30 April respectively) when the correct, now-produced target
is the next free day before Easter (26 March in each case -- Passion
Sunday in 2007/2012, Lent III Sunday in 2057). Verified with actual CLI
output for all three, before and after.
Also cites RG 96 Attamen (b), the same primary-source passage, as the
direct authority for All Souls' own move to the following Monday when
impeded by a Sunday -- previously inferred from RG 91 entry 8's
parenthetical plus the general walk, which happened to produce the
right date; now stated directly.
Rewrote the two existing Annunciation unit tests, whose synthetic
occupants no longer trigger the (now correctly conditional) exception,
and added a 2057 regression test using the real Temporal_ef.temporal as
occupant plus a real-data cram pin -- both mutation-verified against
the unconditional reading.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
search_from could walk up to 400 days past origin before Calendar's own
~start ~stop clamp is ever consulted, and nothing stopped it probing
occupant on a date past 31 December 9999 -- occupant chains through the
real EF rite's temporal, which calls Computus.gregorian_easter, not
total outside 1583..9999 (it Date.makes and failwiths on Error).
Not reachable with the shipped sanctoral data alone, but reachable
through the project's own primary extension path: an overlay adding an
I-class feast on 25 December leaves nothing but Class2 Nativity-octave
days for the rest of civil year 9999, so the unguarded search reached 1
January of year 10000 and crashed there with 'computus: year 10000 out
of range 1583..9999'. 9999 is an in-range year and the kernel's contract
is 'never raises on in-range input'.
search_from now also stops, without probing occupant again, once it
passes Date's own domain ceiling -- the same 'return a finite date, let
Calendar's own out-of-range handling record it, never pretend to have
found something admissible' contract the existing step-count guard
already follows.
Two new tests, both mutation-verified to actually reproduce the crash
when the guard is removed (see the task report): a precedence_ef.ml unit
test using the real Temporal_ef.temporal as occupant (a synthetic
occupant can never discriminate this, since it never calls Computus
itself), and a Calendar-level integration test reproducing the exact
overlay-based scenario the review found.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Assembles Rite_ef.context (lib/rites/rite_ef/rite_ef.ml[i]): temporal,
anchors and vocab from Temporal_ef; rules from Precedence_ef's band,
disposition and admit; season_runs = Vocab_ef.seasons; transfer_target
newly implemented here.
transfer_target (RG 96): the next following day that is not I or II
class, with the Annunciation's own exception (Monday after Low Sunday).
Terminates by a structural step bound on its internal search, independent
of Calendar's own round guard, which bounds rounds across a year, not one
call's walk; documented as an obligation on rite.mli's transfer_target
field, which did not previously state it.
Fixes the vigil-naming mismatch Task 7's review predicted: the sanctoral
bootstrap names its vigils with a vigil-of-X prefix (lectio's own
convention), while Precedence_ef's is_vigil only recognised the temporal
cycle's own X-vigil suffix. Both are now recognised, fixing RG 91 entries
21/26 and RG 33's vigil omission for the four affected celebrations.
Verified by unit test and by mutation-testing the fix (reverting it fails
exactly the new rows) and against real output across several years.
Suppresses data/ef/sanctoral.sexp's vigil-of-christmas via a new overlay,
data/ef/adjustments.sexp: it is the same celebration as the temporal
cycle's own ef-nativity-vigil, both dated 24 December.
colitur day <year>: one line per civil-year day, temporal and sanctoral
fully resolved through Layer, Overlay, Precedence_ef and Calendar -- the
first CLI path exercising the whole Plan 3 pipeline against real data.
Verified the All Souls transfer chain (Tasks 7-8-11) end to end against
real output for both a Sunday year (2025, lands on 3 Nov) and a
non-Sunday year (2026, observed directly on 2 Nov).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
disposition's Class1 branch was unconditional on rank, so an impeded
I-class Sunday (Advent/Lent/Passiontide/Low Sunday) currently
transferred like a feast. RG 95 (register lines 323, 363) restricts the
right of translation to I-class FEASTS -- RG 91's own table lists
Sundays as a separate row (entry 6, line 332) from feasts (entries
11-13, lines 337-339) -- and RG 109(a) (line 374) lists "of a Sunday"
as a privileged commemoration category, which presupposes an impeded
Sunday stays put rather than moving to another day.
Excludes is_sunday_slug losers from the Transfer branch so they fall
through to the existing Commemorate (privilege_of loser) branch, which
already tags them Privileged via RG 109(a) with no further change.
Fires on real dates in the 2005-2050 differential window: 24 December
on Advent IV in 2023, 2028, 2034, 2045; 8 December on an Advent Sunday
in 2024, 2030, 2041.
Verified no previously-passing Transfer assertion used a Sunday-shaped
loser (grepped test_precedence_ef.ml, test_calendar.ml -- which uses
its own synthetic rite unrelated to Precedence_ef.disposition --
test_colitur.ml, test_validate.ml); confirmed by mutation-reverting the
fix and observing exactly the one new test fail, nothing else.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Precedence_ef.privilege_of classifies a commemoration candidate against
RG 109's closed list of privileged commemorations (a Sunday; a I-class
day; a day within the Octave of the Nativity; a September Ember day; a
feria of Advent, Lent or Passiontide; the Major Rogations in Mass), read
entirely off the candidate's own rank/slug/origin, no context needed.
Major Rogations (f) is left unimplemented rather than guessed: no
producer for the Major Litanies exists anywhere in this codebase yet.
disposition's two Commemorate sites now call privilege_of instead of
Task 8's interim_privilege placeholder, which is removed entirely
(binding and .mli export both gone).
Precedence_ef.admit applies RG 111's four admission counts, keyed on
the observed day's own class and Sunday-ness: a I-class day admits none
except one privileged commemoration; a II-class Sunday admits one, but
a privileged commemoration due displaces any ordinary one regardless of
dignity; any other II-class day admits one by dignity alone, with no
such override; III/IV-class days admit at most two by dignity. Ties
break on slug, matching Precedence.compare_by, so the admitted set
never depends on input order. Every admitted candidate is a value taken
unchanged from the input list, never rebuilt, so Precedence.resolve's
physical-equality-based dropped/omitted accounting keeps working --
closing a note left open since Task 2.
Tests: RG 109 category rows (one per reachable category, plus boundary
rows proving Advent/Lent Ember days and Minor Rogations are correctly
excluded), RG 111 admission rows checked on slug identity rather than
count, an order-independence check, and three end-to-end integration
tests proving the admission limit's drop lands in resolution.omitted
rather than vanishing.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Review finding: the comment claimed every RG 91 entry that can outrank a
II-class vigil (entry 21) without being a Sunday is I class 'by the table's
own structure (entries 1-13)'. False -- entries 14 and 16-20 (Feasts of the
Lord II class, universal/proper/indult II-class feasts, days within the
Nativity octave) are all Class2, all outrank entry 21, and none is a Sunday.
The code was always correct: impedes_vigil implements RG 33's own two named
conditions (any Sunday, or a I-class feast) directly, and does not depend on
the band table's numeric ordering at all. Reworded to say so, citing the
counter-example entries the review named instead of appealing to a table
structure that does not guarantee what the old comment claimed.
Comment-only change; no logic, signature, or test changes.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Precedence_ef.disposition decides the loser's fate in an occurrence: a
Commemoration_only celebration is always commemorated (it can never win or
transfer); a I- or II-class vigil impeded by any Sunday or a I-class feast
is entirely omitted (RG 33), checked before the generic rule below or the
Nativity/Pentecost Vigil could wrongly transfer; any other I-class loser
transfers (RG 95 -- only I class has the right of translation); everything
else is commemorated, with the admit-or-omit decision left to RG 108-111's
admission count (Task 9). RG 94 needs no code: resolve always compares a
loser against the day's actual winner, never against a departed sibling, so
no commemoration can ride along with a transferred feast in this design.
This is the branch that completes Task 7's carried All Souls fix: once it
loses to an occurring Sunday, its untouched Class1 rank routes it to
Transfer via the generic rule, not a special case. Landing on 3 November is
Rite.transfer_target's job, not wired up yet.
Commemorate carries an interim Precedence.Ordinary privilege pending Task
9's RG 109 implementation, exposed as interim_privilege for that task to
replace.
Table-driven tests cover each rule, including RG 33's boundary from both
sides and a Commemoration_only loser that is also Class1 and vigil-shaped
to pin the branch ordering. Mutation-tested: disabling RG 33, either
direction of RG 95's rank condition, or the Commemoration_only priority
check each fail exactly the rows built to catch them.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Review of 436ba75 found two calendar defects and one coupling risk.
Entry 8 (All Souls) dropped register line 334's own qualifier,
"(yields to an occurring Sunday)" -- it returned 8 unconditionally,
so on 2 November falling on a Sunday (2025, 2031, 2036, 2042 in the
2005-2050 differential window), All Souls incorrectly outranked and
observed over the Sunday. 2 November is always Time_after_pentecost
and never coincides with any other entry's own date, so the only
rival this exception ever has is an ordinary entry-15 II-class
Sunday; on such a Sunday, band now returns one worse than entry 15's
own value rather than the literal 8 -- strictly worse, not merely
different, since an exact tie would fall to Precedence.resolve's slug
tie-break, which for "ef-all-souls" against a Sunday slug would make
All Souls win the tie anyway. Entry 8's own rank is untouched, so
Task 8's disposition still sees a true I-class candidate to transfer.
Entry 14 (Feasts of the Lord, II class) added a universal-layer
restriction the register's line 341 does not carry -- contrast entry
16 at line 342, which explicitly says "not of the Lord"; RG 37c
(register line 393) also speaks of "II-class feasts of the Lord"
with no universal qualifier. Dropped the layer test: a proper or
indult feast of the Lord now bands 14, not 19/20.
Also exposes vigil_suffix and ember_prefixes from precedence_ef.mli,
matching universal_layer/indult_prefix's existing "colitur convention,
not an RG citation" treatment -- both were previously private literals
duplicated in the test file, so a rename of temporal_ef.ml's slug
format could have drifted silently past both sides agreeing with each
other. Two test rows now build their candidate from Temporal_ef.temporal's
own real output (entry 18's Lent Ember day, entry 21's Ascension Vigil)
instead of a hand-typed slug, closing that specific coupling.
Adds the three cheap rows review named as closing the remaining
unpinned guards (a temporal-origin Class1 candidate on an unnamed
date; a universal Class2 vigil of the Lord; a non-universal Class3
vigil), plus a resolve()-level test proving the Sunday is actually
observed over All Souls, not just that band returns the right integer
in isolation.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Precedence_ef.band transcribes RG 91's 28-entry Table of Precedence
(rules-register.md §4) for the EF rite: given a day's context and a
candidate celebration, returns the table's own entry number, 1-28
(I class 1-13, II class 14-21, III class 22-26, IV class 27-28); lower
wins. Every branch carries its entry number and register citation in a
comment, checked in the table's own numeric order.
Two entries are transcribed as the register states them even though
they invert the pattern the rest of the table follows: at III class,
23 (particular calendars) outranks 24 (universal), the reverse of how
11/12 and 14/16/19/20 rank a universal feast ahead of a proper one at
I and II class.
Sanctoral-origin, layer-decided entries (11-13, 14/16/19/20, 23/24)
follow the brief's structural insight: a celebration whose layer is
not the universal base is an overlay -- proper, or indult if its
layer id also carries the indult prefix. Neither the universal-layer
id nor the indult prefix is an RG citation; both are colitur's own
data-modelling convention, exposed from the module so whichever task
loads the real EF sanctoral overlays can align to them.
Vigils (21, 26) are read off the temporal cycle's own -vigil slug
suffix rather than gated on origin, since a II/III-class vigil can be
either temporal-origin (Ascension, already produced by temporal_ef) or
sanctoral-origin (a saint's vigil, no task has loaded yet); Ember days
(part of entry 18) are read off temporal_ef's own ember slug prefixes
rather than re-derived, since the September anchor is independently
flagged there as one of the more contested dates in the calendar.
A candidate shape the table has no row for (e.g. a Class1 vigil that
is not Nativity or Pentecost, or a Class4 candidate marked as a vigil
-- RG 91 has no IV-class vigil either) returns a dedicated unclassified
sentinel (max_int) rather than being folded into a same-rank entry it
does not belong to.
test_precedence_ef.ml is table-driven: one Alcotest.test_case per RG
91 entry (55 rows total, several entries covered by more than one
named day so a single missed offset cannot hide behind a passing
sibling), each date computed from Computus.gregorian_easter rather
than hand-typed, so an arithmetic slip cannot pass by accident.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Four findings from Task 6 review, addressed on top of f15e44d.
1. transferred_out was a single Date.t option, so when RG 97-98 collides
three or more feasts on one date (more than one loser), only the last
one Hashtbl.iter happened to visit survived -- a genuinely lost move,
and which one survived depended on OCaml's hash seed
(OCAMLRUNPARAM=R), an environment read a kernel invariant forbids.
RG 97-98 says coinciding feasts transfer "in order" -- plural -- so
the type was wrong, not the fixture: transferred_out is now
(Celebration.t * Date.t) list. transferred_in stays a single option,
deliberately: a day receives at most one arrival (RG 96 sends each
departure to the next non-I/II-class day, and the first to arrive
occupies it). The per-day list is canonicalised (sorted by target
date, then slug) after accumulation, the same fix layer.ml already
applies to its own date-bucket index and for the same reason.
Verified clean across 15 runs under OCAMLRUNPARAM=R; disabling the
canonicalisation step showed the raw order genuinely flip between
seeds, confirming the fix is load-bearing.
2. Every deferred candidate in the fixture was the same rank, so
compare_deferred's band branch was unreachable and reversing it broke
nothing -- the RG 97-98 test was pinning slug order, not band order.
The fixture now has three ranks (Hi1 outranks Hi2, both transfer,
both outrank Lo), with slugs chosen so band order and slug order
disagree. Reversing the band comparison now fails the test on
"higher-band loser claims 2 Feb first", received the wrong slug
instead.
3. A transfer_target free to name any date could place a candidate
outside the liturgical year's own start/stop bounds: invisible to
year/build_day, so it would be observed nowhere and, since its
origin's re-resolution would report it as settled, omitted nowhere
either -- genuinely gone, contradicting calendar.mli's "never
silently dropped". place_transfers now checks the range on every
placement and routes an out-of-range one to a permanent-exclusion
table instead of assignment, with its own cited omitted reason.
4. Precedence.resolve folds Transfer and Repose into one deferred case,
and place_transfers routed all of it through transfer_target (RG
96's search), which is only correct for Transfer. Repose is RG
100-102's repositio, a distinct rubric this module does not
implement. Documented rather than split into a second mechanism:
nothing in the EF ruleset returns Repose (design spec section 1.3,
"declared, not exercised"), so the gap is latent, not a live bug.
Two new tests (origin records every departure; transfer target outside
year is recorded not lost); the RG 97-98 test's fixture and assertions
rewritten for finding 2.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Precedence.resolution already tracked what happened to every losing
candidate -- commemorated, deferred, or omitted with a reason -- but
Liturgical_day.t had nowhere for the deferred and omitted buckets to land,
so Calendar dropped them at the door. Task 12's no-celebration-lost
invariant needs to read that accounting off the day result itself, not
re-resolve every day to reconstruct it, so a reason recorded nowhere is not
recorded.
Add Liturgical_day.omitted : ('r Celebration.t * string) list, after
transferred_out and before citations. Calendar.resolve_day now folds
resolution.omitted (Precedence's own native omissions, reasons intact) and
resolution.deferred (mapped to "deferred: transfer placement not yet
implemented (Task 6)") into it.
Adds a full-day accounting test against the whole Calendar pipeline: four
colliding sanctoral entries plus the day's feria, checked as a slug set
(matching test_precedence.ml's own nothing-silently-lost test) so a
candidate silently dropped or duplicated into two buckets would fail it,
plus an identity check that the deferred and admission-limit reasons don't
get swapped.
|
| |
|
|
|
|
|
|
|
|
|
| |
Transfers make per-date resolution impossible to do correctly: resolving 25
March can push a feast onto 26 March, and RG 97-98 has coinciding I-class
feasts transfer in table order, which needs global knowledge. So year computes
a whole liturgical year in one pass and day indexes into it. Pure, no cache, no
mutable state.
This commit resolves each day but does not yet place deferred transfers; they
are recorded with a reason. Task 6 adds the placement pass.
|
| |
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
| |
Temporal is embedded rather than flattened, so season/week/weekday have one
home and cannot disagree with themselves. transferred_in/out make transfers
visible in the result -- an ordo must print 'transferred from the 25th', and
the nothing-lost invariant reads these fields. citations exists and is empty
until Plan 4; adding it later would widen a type every consumer matches on.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Three rite-supplied functions, not one: band (who wins, RG 91), disposition
(what happens to the loser, RG 92-95) and admit (how many commemorations are
admitted, RG 111). The loser's fate depends on the loser's own rank, so
conflating them would resist extension.
resolve takes the temporal candidate separately from the sanctoral list, which
makes it total by construction. Every candidate lands in exactly one of
observed, commemorations, deferred or omitted -- nothing is dropped silently,
which is what makes the no-celebration-lost invariant checkable.
|
| |
|
|
|
|
|
| |
The 1960 reform reduced many feasts to a bare commemoration. They keep a rank,
because RG 111 orders admitted commemorations by dignity, but they can never be
the observed day. Modelled as a separate status rather than a fifth rank: RG 8
fixes the classes at four.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two correctness bugs in Temporal_ef, both only visible across many
years, not a single point assertion:
1. Duplicate slugs within one liturgical year. Christmastide has no
numbered weeks, so the ferial fallback's <season>-<week>-<weekday>
scheme collapsed every feria in it to literal week "0". Since
colitur's Christmastide runs 25 Dec - 13 Jan (RG 72-73, a deliberate
divergence from lectio), the same weekday recurs across that span
and the keys collided (e.g. ef-christmas-0-saturday on three
different dates). Fixed with a dedicated christmastide_feria_slug
that splits the span into four sub-stretches, prioritising
compatibility with lectio's own keys wherever lectio has one:
- 26-28 Dec keeps lectio's existing ef-christmas-0-<weekday>.
- 2-5 Jan becomes ef-christmas-1-<weekday> -- lectio collapses
this indistinguishably into the same key as the stretch above,
so there is nothing to preserve; a colitur-only lectionary gap.
- 7-13 Jan splits at the actual first-Sunday-after-Epiphany
origin: on/after it, this is genuinely week 1 of Time after
Epiphany and takes lectio's own ef-time-after-epiphany-1-<weekday>
(which also can't collide with that season's own later week-1
ferias, since it's the same computation). Before it (0-6 days,
whenever Epiphany doesn't fall on a Saturday), a first attempt
at labelling this "week 1" too, matching a literal reading of
lectio's behaviour, was verified empirically (a throwaway sweep
of the full 1583..9998 domain) to reproduce duplicates in most
years -- so this remainder is its own ef-christmas-2-<weekday>,
a further colitur-only gap.
Verified with the same throwaway sweep: zero duplicate slugs across
1583..9998 after the fix.
2. named's week field was set by hand on some branches (Passion/Palm
Sunday, Easter, Low Sunday, Pentecost and its Vigil, Christ the
King) and left at None on others (Ascension and its Vigil, Corpus
Christi, Sacred Heart) even though all of them sit inside a
numbered season run. named no longer carries a week at all --
temporal now calls week itself for every day, named or not, so "a
named day inside a run carries that run's week" holds by
construction rather than by remembering to set it on each branch.
temporal.mli's week field doc is reworded to state the actual rule.
Covering tests: point assertions for all four Christmastide
sub-stretches; a QCheck property scanning random years for any
duplicate slug within a liturgical year (excluding the deliberate
resumed-Sunday reuse); point assertions for the four previously-None
days now carrying their week (first to fail without the fix: Ascension
Vigil, expected Some 6, got None); a QCheck property asserting
temporal's week equals week for every day of the year, replacing a
prior property whose fallback made it structurally incapable of
detecting an omitted week.
Also: cite RG 91 e18 for the September/Advent Ember days matching
lectio and flag the Lent/Whitsun Ember and Rogation slugs as
colitur-only lectionary gaps inline, matching the existing
Nativity-vigil/octave-day convention; cite RG 117/123/127/128/131 for
season colours and Gaudete/Laetare rose; drop the unreachable
Passiontide arm from the Sunday-rank match (Passiontide has only two
Sundays and both are already named above, so no Passiontide Sunday
ever reaches that fallback).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Layer.load narrowed its catch to Sexplib0.Sexp_conv_error.Of_sexp_error,
but rank_of_sexp is caller-supplied and may raise anything -- e.g. a
hand-written rank parser that calls invalid_arg. Overlay.load already
catches every exception from the equivalent call; mirror that here so
layer.mli's "never as an exception" promise actually holds.
Date_spec.t derived its sexp converters with plain ppx_sexp_conv, unlike
Slug and Lang, which hand-write validating parsers specifically so
malformed data is rejected at load. (Fixed (month 13) (day 40)) used to
deserialise cleanly into a spec that simply never resolves -- a saint
quietly vanishing with no diagnostic. t_of_sexp now re-runs the value
through the existing fixed validator, the same shape Slug and Lang
already use.
Covering tests: a Layer.load case where rank_of_sexp raises
Invalid_argument instead of Of_sexp_error (would have escaped
uncaught before this fix); two Date_spec.t_of_sexp cases (month 13,
31 April) that must raise Of_sexp_error rather than silently
constructing an unresolvable spec.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
named hardcoded week = None for ef-pentecost-vigil, ef-pentecost and
ef-christ-the-king, unlike the five sibling named Sundays (Easter, Low
Sunday, Passion Sunday, Palm Sunday, Trinity), which already carry the
explicit week number that week computes for the same date. Because these
three sit inside a season run rather than at a run boundary, the omission let
the week number jump on the following Monday without an intervening Sunday
marking the change -- breaking week continuity in every single year in the
domain.
This was found by the new Validate invariant harness (Task 14), which
flagged exactly two failures in every one of the 8416 years 1583..9998, both
on the Monday after one of these days. Season contiguity itself was clean
across the whole domain; only week numbering was affected.
Pentecost and its Vigil are fixed Easter offsets, so they take the literal
values (7 and 8) that week already computes for them. Christ the King's date
varies by year, so the same expression week evaluates is inlined instead,
since week is defined later in the file and cannot be called from named.
Added a general property test, prop_named_week_agrees_with_week, checking
across 200 random years that wherever named gives an explicit week, it
matches what week independently computes for that date -- covering all
sibling entries, not just the three fixed here, so the same drift cannot
silently reappear in a future addition to named.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Ferial ranks per RG 91: I class in Holy Week and the privileged octaves
(entries 7, 10), II class for Advent 17-23 and the Ember days (entry 18), III
class for Advent to 16 December and Lent/Passiontide (entries 22, 25), IV class
per annum (entry 28). Two further divergences from lectio, both from the table:
Advent 17-23 are II class, and the Lenten Ember days are II class rather than
ordinary Lenten ferias. Rogations (RG 80/87) exist at all, which lectio does
not compute. Gaudete and Laetare are rose.
Two corrections against the primary source beyond the task brief's draft.
Entry 7 (verified against the Latin: "feria IV cinerum et II, III et IV
Hebdomadae sanctae") covers only Ash Wednesday and Monday-Wednesday of Holy
Week; Thursday-Saturday are the Sacred Triduum, entry 2, cited separately.
Rogation Monday and Tuesday keep the ordinary ferial rank of their season
(RG 88: "de Litaniis minoribus nihil fit in Officio") rather than a fixed
class, since no RG 91 entry ranks them specially.
A module-type constraint proves Temporal_ef satisfies the kernel RITE contract.
|
| |
|
|
|
|
|
|
|
| |
Task 12 review: add a permanent regression test for floor_div's negative
branch (Ash Wednesday, 4 days before the Lent I origin -- the only date in
the system where it fires), and stop sunday_slug from independently
recomputing the Pentecost-relative week number that week already computes.
A cross-check test pins the two together before the refactor and continues
to guard against future duplication.
|
| |
|
|
|
|
|
|
| |
Week origins are Sundays, so week numbers are constant Sunday-to-Saturday; the
origin is season-specific, which is why Validate must not assume a run starts
at 1. When Easter is early the surplus Sundays after Pentecost resume the
Sundays after Epiphany that Septuagesima cut short, and the last Sunday before
Advent always keeps the 24th (Last) Mass.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Circumcision (1 Jan) is the Octave Day of the Nativity, RG 91 entry 5, the
same table entry as the Nativity vigil -- not covered by the entries-1-3
group it sat under. Passion Sunday, Palm Sunday and Low Sunday are RG 91
entry 6 (I-class Sundays of Passiontide and Low Sunday); Ash Wednesday is
entry 7 (I-class ferias). All five branches now carry their own citation,
and the function-level comment states the full coverage instead of a
narrower one.
Also corrects the Nativity-octave range from RG 64-70 to RG 63-70, matching
the register's own citation for the octave rules.
Comment-only change; behaviour is unaffected.
|
| |
|
|
|
|
|
|
|
|
| |
The I-class feasts of the Lord (RG 91 entries 1-3), the Nativity, Ascension
and Pentecost vigils (RG 28-34), and the days within the Octave of the
Nativity (RG 64-70). Christ the King is the last Sunday of October per the
1960 calendar, not the OF's last Sunday before Advent.
Rogation Wednesday coincides with the Ascension vigil; with no precedence
framework until Plan 3, temporal emits the higher-ranked vigil.
|
| |
|
|
|
|
|
|
| |
Christmas Time runs to 13 January inclusive (RG 72-73) and time after Epiphany
opens on 14 January (RG 77), diverging from lectio, which starts
time-after-epiphany at 6 January. Holy Saturday stays in Passiontide: the
Easter Vigil is a night Mass, and a per-day calendar assigns a date by the
season governing its day-hours.
|
| |
|
|
|
|
|
| |
Eight seasons in canonical liturgical-year order, each carrying its RG
citation, and the four classes. season_slug_word is deliberately distinct from
season_to_string: slugs are lectionary keys adopted verbatim from lectio,
which calls Paschaltide 'easter' and Christmastide 'christmas'.
|
| |
|
|
|
|
|
|
|
| |
Make alignment impossible: both headers and to_row are derived from a single
list of (name, extractor) pairs, so reordering one without the other is
impossible. CSV schema cannot silently mislabel columns.
Add a meaningful test that pins the header names in order, not just length.
This catches column reordering regressions.
|
| |
|
|
|
|
|
| |
The boundary where rite-parametric types stop. CSV, JSON and the template
engine all render from this one schema, so they never see a type variable.
headers/to_row cover the scalar columns; names and citations are
variable-arity and wait for the richer encoders in Plan 5.
|
| |
|
|
|
|
|
| |
add/suppress/replace/field-edit folded in order, last writer wins per field,
empty the identity. A directive naming an unknown slug, or adding one that
already exists, yields a diagnostic rather than silence or a hard failure:
overlays must survive a shifted base while still surfacing authoring errors.
|
| |
|
|
|
|
|
| |
Entries sort by slug so equal layers serialise identically. The by-date index
is built once per layer rather than per year, since fixed dates are
year-independent; a full-domain sweep would otherwise rescan every entry for
every day. load turns parse and validation failures into result.
|
| |
|
|
|
|
|
|
|
|
| |
The comment explaining Celebration's single type parameter claimed OCaml
rejects a type variable that appears in no field. That is false: a phantom
season parameter compiles cleanly. The real reason is a design choice, not a
compiler constraint -- a celebration has no season of its own (season is
contextual to the day, and lives in Temporal.t), and a phantom parameter
would carry no information while forcing every consumer to thread a
meaningless variable.
|
| |
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Use [@@deriving sexp] with open Sexplib0.Sexp_conv instead of hand-rolling
converters (fixes non-standard Citation shape and missing field validation).
Names.t_of_sexp wraps derived version to enforce canonical sort on load.
Add tests:
- Names of_list duplicate handling
- Names sexp canonical sort guarantee (identical serialization)
- Names and Date_spec sexp roundtrips
Fix Names.remove to canonicalize output (defensive against non-canonical input).
|
| |
|
|
|
|
|
| |
Names is an open language-keyed assoc kept in canonical order so equal name
sets serialise identically. Citation carries references only, never text.
Date_spec ships the one form the EF sanctoral needs; 29 February is
constructible and resolves to None in common years.
|
| |
|
|
|
|
| |
Both parse through a smart constructor returning result, and both hand-write
t_of_sexp so a malformed value in a data file is rejected at load rather than
silently accepted -- deriving the converter would have bypassed validation.
|
| |
|
|
|
|
| |
The six liturgical colours and the Lord/BVM/saint/temporal distinction are
common to both Roman forms, so they are shared closed variants rather than
rite-parametric. Subject is so named because class is an OCaml keyword.
|
| |
|
|
|
|
|
| |
Adds sexplib and ppx_sexp_conv to the project and wires the ppx into the
kernel library. Date's sexp form is an ISO-8601 atom rather than the opaque
rata die, so data files stay human-editable and parsing revalidates the
1583..9999 domain.
|
| |
|
|
|
|
| |
Ash Wednesday/Palm Sunday/Ascension/Pentecost/Corpus Christi as Easter+/-N.
Verified vs 2026 dates; exhaustive weekday invariants 1583..9999 (Ash Wed=Wed,
Ascension/Corpus Christi=Thu, Palm/Pentecost=Sun).
|
| |
|
|
|
|
|
| |
Anonymous Gregorian (Meeus/Jones/Butcher) for OF+EF; Meeus Julian mapped to the
proleptic-Gregorian date for future eastern rites. Verified vs known dates
(2000/2024-27, 1583; Orthodox 2023/24) and EXHAUSTIVELY over 1583..9999: every
Easter is a Sunday in [Mar22,Apr25].
|
| |
|
|
|
|
|
| |
Hinnant civil<->days rep (1970-epoch rata die); make validates month/day and
the 1583..9999 domain; of_rata/add_days are total arithmetic. Weekday, compare.
Tested: known weekdays, leap boundaries, rejects; qcheck round-trip / add-inverse
/ weekday-cycle properties over random in-range dates.
|
|
|
OCaml 5.2.0 local switch; colitur_kernel library, a colitur executable stub,
and an alcotest+qcheck test runner. AGPL LICENSE, README, generated colitur.opam.
|