| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
precedence, not slug
Precedence_ef.admit broke a same-rank tie among commemoration candidates
alphabetically by slug -- a deterministic engineering convention with no
rubrical citation. RG 113's own second sentence, primary-source-verified
against two independent scans and previously uncited in the register
(only "commemoratio de Tempore fit primo loco" was quoted before), gives
the real rule: "in admittendis et ordinandis aliis commemorationibus,
servetur ordo tabellae praecedentiae" -- admitting and ordering
commemorations both run on the rite's own table of precedence (band's
28-entry table), not RG 8's coarse four-class rank.
Precedence.resolve now computes each commemoration candidate's own band
value once, generically, and hands it to rules.admit as a third tuple
element (Precedence.rules.admit's signature changed accordingly, ditto
Precedence_ef.admit; every rule-record stub in the test suite updated to
match). Precedence_ef.admit's own compare_dignity is replaced by
compare_precedence, ordering by band then slug; a residual tie within one
identical band value still falls back to slug, since RG 113 gives no
further instruction there -- documented as a still-uncited engineering
convention, not dressed up as a rubric.
RG 98 ("in paritate autem Officium prius impeditum praecedit") was
considered as a candidate authority for that residual and rejected: it
governs the transfer queue order among several simultaneously-impeded
I-class feasts (Caput XIII), a different operation in a different
chapter from RG 113's commemoration admission (Caput XVI); nothing in the
primary text connects the two.
Blast radius measured against the pre-change binary across the entire
1583-9999 domain (not only 2005-2050): the admitted-commemoration-slug
set is byte-identical, day for day, before and after this change. The
fix corrects the citation and mechanism, not the answer, on this
codebase's current data -- both of the task brief's named examples
(22 Feb Chair-of-Peter/Lent-vs-Paul, 22 Sept Maurice-vs-Thomas-of-
Villanova) are confirmed present and unchanged in both streams. A new
test (RG113: admit picks by precedence order, not slug, when they
disagree) proves admit actually consults the passed-in precedence value
with a synthetic pair whose slug order and precedence order disagree --
teeth a same-band-only regression test could not have caught, since
every real collision found in the domain happens to agree on both axes.
271 -> 272 tests, all green; COLITUR_EXHAUSTIVE_SWEEP=1 unaffected.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
day's own Sunday-ness, not observed
Fix round 1 review, two findings landed together because each golden pin's
correctness depends on both.
CRITICAL (item 1): reverts this branch's own first-pass retag of the
Purification (2 February) from Lord back to Bvm -> Lord. The calendarium
titles it 'B. Mariae Virg.', but missalemeum -- this project's designated
EF oracle -- treats it as taking an occurring II-class Sunday's place
outright, commemorations empty, exactly RG16(a)'s own festum Domini
treatment (2020-02-02, 2014-02-02, independently fetched). Control,
2019-09-08 (the Nativity of the BVM, an ordinary Marian feast, on a
Sunday): the Sunday observed, the feast merely commemorated -- the
opposite pattern, proving the Purification's own oracle treatment is
deliberate, not a gap. RG16(a) alone, on the untouched data, would have
matched the oracle character for character; retagging Bvm is what moved
colitur away from it on 1200 domain-wide days. The user has ruled: follow
the oracle. Genuine primary-text counter-evidence (RG120(b), the colour
rule grouping 2 February under 'B. Mariae Virg.', a category separate
from 'Domini') is recorded in the register, not discarded -- a different
project could reasonably rule the other way on the same evidence.
most-holy-name-of-mary stays retagged Bvm; that one was never disputed.
Removes the now-unneeded C12 lectio allow-list entry (data/ef/
expected-divergences.sexp, test_differential.ml): with the Purification
Lord again, colitur matches lectio's own class = lord on every affected
date, no divergence to allow-list.
IMPORTANT (item 2): Precedence.rules.admit gains a ~temporal parameter --
a genuine kernel signature change, unlike RG16(a) itself. RG111(b)'s own
Sunday rank floor ('one commemoration, namely of a II-class feast') used
to be detected by reading observed's own slug for a Sunday marker, which
silently assumed observed IS the day's temporal-cycle office. RG16(a)
breaks that assumption on purpose: a Feast of the Lord standing in for an
impeded Sunday 'cum omnibus iuribus et privilegiis' keeps the day a
dominica II classis for RG111(b) too, even though the FEAST, not the
Sunday, is observed. Before this fix an unrelated saint (Pope Sixtus II
et al., 6 August, Class3) was wrongly admitted into the freed slot on
every one of 1178 domain-wide days. Oracle-confirmed directly: 2023-08-06
(a Sunday) shows him displaced, commemorations empty; 2026-08-06 (an
ordinary Thursday) shows him admitted -- being a Sunday is exactly what
excludes him. admit now reads Sunday-ness off ~temporal (the day's own
temporal-cycle candidate, passed through from Precedence.resolve
unchanged) instead of observed. Validate's own admission fixed-point
invariant updated to pass the same real temporal candidate through.
Both golden pins in test_golden.ml were promoted from actual colitur day
output without independently consulting the rubric or the oracle first --
a real defect the review caught. Re-derived from both before re-pinning:
2028-08-06 and 2025-02-02 now pin comms=[], no commemoration at all,
matching missalemeum exactly in both cases.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
precedence_ef.mli said "there is no fifth, unclassified case" after the
same commit renumbered the disposition list from four cases to five; the
count is now six.
precedence.mli's physical-equality obligation described the failure mode
as counting a drop "a SECOND time (once because it is genuinely absent,
once because its identity no longer matches)" -- the same condition
stated twice. What actually happens to a rebuilt candidate record is
that the celebration surfaces in BOTH commemorations (the copy) and
omitted (the original), one admission double-reported. precedence_ef.ml
carried the same muddled sentence, which is where the kernel's copy came
from; both now say it plainly.
vocab.ml/.mli referenced {!Rite_ef.rite_ef.ml} -- a filename inside an
odoc reference, which is malformed. Now plain [Rite_ef.rite].
README documented only `dune test`, so the exhaustive 1583-9999 Validate
sweep was discoverable only by reading test_validate.ml's own comment.
With no CI in this repo, that line is what stands between a committed
artifact and one anyone runs.
No behaviour change: `colitur day` output is byte-identical across 1583,
1900, 1902, 2008, 2011, 2026, 2038 and 9999 (2921 days, both domain
edges). 259 tests by default, 260 with the sweep.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Several comments described work as "arriving with Plan 3" or "waiting
for Plan 3" that either shipped without it or refer to the wrong future
plan number, all now false in ways that would mislead the next reader:
- temporal_ef.ml (two places) and precedence_ef.ml said the Major
Litanies (25 April, RG 80) "arrive with Plan 3's sanctoral". Plan 3
shipped, in this branch, without them; register §6 tracks this as a
plain open item with no plan committed to build it, and now says so.
- temporal_ef.ml said the Sacred Triduum's "own named offices are a Plan
3 sanctoral addition". Wrong on two counts: Plan 3 shipped without
adding them, AND a proper office for I-class FERIAS was never a
sanctoral matter in the first place (RG 21 defines "feria" to exclude
Sundays/feasts, not the reverse). 2026-04-02/03/04 still resolve to the
ordinary Passiontide ferial fallback's own generic slugs
(ef-passiontide-2-{thursday,friday,saturday}), confirmed against real
output; register §6 now records this as its own open item.
- temporal_ef.ml said the Rogation-Wednesday commemoration (Ascension
Vigil day, entry 21) "waits for RG 108-111" -- both the precedence
framework and RG 108-111 exist now; the Wednesday's own commemoration
is still never constructed, but for a different, still-real reason (no
candidate is wired for it), not a forward dependency. Fixed at both of
this comment's two occurrences in the file.
- vocab.ml/vocab.mli's `seasons` field doc said "Validate's contiguity
check reads this" -- false since validate.ml's "seasons" check switched
to Rite.t.season_runs in this branch (rite-supplied, to support a
season appearing in more than one run, which the modern form's Ordinary
Time needs and EF does not).
- vocab.ml/vocab.mli's `ranks` field doc said "it is not a precedence
relation until Plan 3 defines one" -- Plan 3 did define one (RG 111's
dignity ordering), but as its own small, separately-hardcoded function
in precedence_ef.ml, not one derived from this field; corrected to say
so precisely rather than leaving a forward-looking claim unresolved.
- The lectionary bootstrap (reading citations, Liturgical_day.t's own
`citations` field) is called "Plan 3" at slug.ml, vocab_ef.ml, and
three places in temporal_ef.ml, but "Plan 4" at liturgical_day.mli
(whose own doc comment -- "always empty until Plan 4" -- is the
authoritative one: the SANCTORAL bootstrap is Plan 3 and shipped; the
LECTIONARY bootstrap is a separate, later Plan 4). All six corrected to
say Plan 4, cross-referencing the Plan 3/4 distinction at the first
(slug.ml) occurrence so the reasoning is not duplicated six times.
No behaviour change: every edit here is a comment/documentation
correction. Verified byte-identical `colitur day` output across 1583,
1900, 1902, 2008, 2011, 2026, 2038, 9999. 259/259 tests green.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Six independent, small corrections found during the final review:
- dune (workspace root): the comment said the stanza used "(:standard)"
to preserve dune's default `default` alias target; the stanza actually
spells that out explicitly via (alias_rec install). Comment now matches
the code.
- test_validate.ml's test_easter_extremes asserted `List.length ys = 2`
where an identity check was called for -- the comment already named
1598 and 1666, but nothing confirmed extreme_years() found THOSE two
rather than some other pair with the right cardinality. Now asserts the
identities directly (the project's "cardinality where identity was
required" vacuity flavour, per the review).
- test_oracle.ml and expected-divergences-missalemeum.sexp both claimed
"one entry (M13) is [verdict open]" -- M11 is open too (its own verdict
changed from colitur to open in fix round 1); both now say "two entries
(M11 and M13)".
- expected-divergences-missalemeum.sexp's M2 note attributed `band` to
temporal_ef.ml; `band` is precedence_ef.ml's own function.
- lib/kernel/precedence.mli documented `dropped`/`admit`'s physical-
equality obligation nowhere -- it lived only in one rite's own module
(Rite_ef.Precedence_ef.admit's doc comment), but this signature is what
an author of the next rite actually reads. Added the obligation here,
cross-referencing the EF instance as precedent, not the only source.
- README's opam install line omitted sexplib and ppx_sexp_conv (both in
dune-project's own depends; `dune build` fails without them for a
contributor following the README verbatim) and documented only
`colitur easter`, though `temporal` and `day` both exist and are the
more useful entry points. Fixed both.
No behaviour change: comment/doc/test-assertion corrections only (the
easter-extremes fix strengthens an assertion, it does not change what
passes). Verified byte-identical `colitur day` output across 1583, 1900,
1902, 2008, 2011, 2026, 2038, 9999. 259/259 tests green.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
| |
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.
|