summaryrefslogtreecommitdiff
path: root/test/test_overlay.ml
Commit message (Collapse)AuthorAgeFilesLines
* feat(overlay): default citations and layer, humanise parse errorsLukasz Kasprzak2026-08-181-1/+65
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | A user-supplied overlay is the only sexp this engine reads that a human writes by hand, and two of Celebration.t's eight fields carry nothing such an author can meaningfully supply: citations is always empty for a local feast, since citations come from the rite's lectionary and never from calendar data, and layer merely repeats the overlay file's own id. Requiring both made the commonest first mistake -- omitting them -- fail with 'lib/kernel/celebration.ml.t_of_sexp: the following record elements were undefined: citations layer', which names a source file the author will never open and does not say what to write instead. A minimal local feast needed 12 lines, two of them noise. Overlay.load now fills each field only where ABSENT, so an explicitly stated value always wins: an overlay may legitimately name a layer different from its own id, and defaulting must not silently overwrite that. A test pins both directions. Deliberately scoped to overlays. Layer.load, which reads the shipped sanctoral, is untouched and stays strict -- that data is the project's own, every field of it is asserted by tests, and a missing one there is a defect rather than a convenience. Parse failures also stop naming kernel source paths: the five prefixes that actually reach a user are rewritten into the vocabulary of the file being edited, and anything unrecognised passes through verbatim rather than being reworded into something possibly wrong.
* feat(kernel): movable Date_spec variants, and Rogation WednesdayLukasz Kasprzak2026-08-171-5/+8
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Date_spec carried one variant, Fixed(month, day), and its own header said Sunday- and Easter-relative forms would arrive with the OF sanctoral. They arrive early because two things needed them at once: a user-supplied overlay carrying a local movable feast had no way to express "the first Sunday of October", and Rogation Wednesday's commemoration has been recorded as architecturally blocked since 2026-08-13. That blockage turns out to have been a premise, not a fact. The register concluded the Rogation "is not Commemoration_only sanctoral data by nature... it needs a genuinely THIRD kind of thing this architecture has no name for" -- but the reasoning rested on there being "no civil (month, day) pair to anchor a Fixed entry to". That is a statement about Date_spec's expressiveness. With Easter_offset the entity is ordinary sanctoral data, the identical shape Add major-litanies already ships, differing only in how the date is written. No third channel was needed. The register is corrected rather than left contradicting the code. Two variants: Easter_offset of int, and Nth_weekday of {month; nth; weekday} with negative nth counting from the end. Fixed's sexp representation is untouched, so all 327 sanctoral entries and the whole overlay parse unchanged. Both are re-validated in t_of_sexp through their smart constructors, extending the idiom Fixed already used -- the failure that guards against is invisible, a spec deserialising into something that silently never resolves and a celebration vanishing with no diagnostic. Easter is supplied by the rite, not computed in the kernel. Computus ships Gregorian and Julian both, and picking one here would hard-code a Roman assumption into rite-agnostic code and be silently wrong for a Julian-reckoning rite. Rite.t gains an easter field; Rite_ef supplies the Gregorian one. Layer keeps a split index. Fixed entries stay in the year-independent (month, day) table, which preserves the fast path and the "30 November counted twice in a 371-day span" behaviour validate.mli documents for St Andrew. Movable entries have no year-independent key by construction -- the same spec lands on a different (month, day) each year -- so they resolve per civil year into a rata-die table. index_by_date is removed rather than kept as an alias: two ways to index a layer, one of which silently ignores movable entries, is the trap this change exists to avoid. Two domain-edge defects, both found by the suite rather than reasoning. A liturgical year is Advent-anchored, so resolving civil year y names y +/- 1, and at the edges those are 1582 and 10000, which Computus correctly refuses by raising. The ceiling surfaced through the domain-ceiling test, the floor through `colitur day 1583` failing outright. Fixed once, in Layer.index, the single point that calls the rite's easter -- a clamp repeated per caller would have been two places to get wrong a third time. Rogation Wednesday: RG 87 assigns the Litaniae minores to the Monday, Tuesday AND Wednesday before Ascension (scan1:691); colitur built the first two from the start and never the third. RG 88 keeps it out of the Office, so the Ascension Vigil remains observed. RG 89 routes it through the Major Litanies' rules, but RG 109's closed list names only maiores, so by RG 107 it is an ORDINARY commemoration -- confirmed in the output, not assumed. Name from the Missal's own "feria IV" (scan1:20495-20497), violet to match the Monday and Tuesday offices. Blast radius, full 1583-9999 sweep against the pre-change binary: 2257 days, every one the same shape, zero unclassified, line counts equal both sides. The complement is the informative half -- in the other 6160 years the Rogation does not appear at all, because RG 111 admits one commemoration on a II-class day and an impeded feast takes it under RG 113, a Commemoration_only candidate having no row in RG 91's table. Both shapes are pinned. Allow-lists unmoved: layer 3 compares no commemorations and is blind by construction, and layer 4's 2026-2027 window has a competing feast on both Wednesdays, so the Rogation is capped out in exactly the years the oracle can see. C8 is unchanged, asserted by its own count pin. Not built: Sunday-relative specs, which nothing needs yet, and the --overlay plumbing that would let a user actually supply a local movable feast. Nth_weekday therefore ships with synthetic and property coverage only; inventing a shipped entry to exercise it would be fabricating calendar data. Register: section 6.10.
* kernel: loaders never escape as an exception, date-spec validates on loadLukasz Kasprzak2026-08-111-0/+19
| | | | | | | | | | | | | | | | | | | | | | 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.
* test(overlay): close review gaps in the merge-algebra test suiteLukasz Kasprzak2026-08-111-6/+97
| | | | | | | | | | | | | | | | | | | | | | | | | Five review findings, all in the test suite the task designates as the algebra's specification: - test_diagnostics's headline assertion was vacuous: it checked slug presence after an add-over-existing, but hilary was already present before the Add ran, so a broken implementation that recorded the diagnostic and skipped applying the entry would still pass. Add now carries a rank that genuinely differs from base()'s default, and the assertion checks the field actually changed. - Set_subject, Set_name, Remove_name, Set_citation and Remove_citation were implemented but never exercised. Added coverage for all five, including the case that matters most for Set_citation: setting a part that already exists must replace it, not append a duplicate. - Replace had zero coverage. Added tests for both its existing-slug path (entry swapped, no diagnostic) and its absent-slug path (diagnostic recorded, entry still added). - merge's diagnostic accumulation was untested; test_merge_order discarded the diagnostics it got back. Added a test asserting two overlays' each producing one diagnostic come back in application order. Extended the cel/entry test helpers with optional subject/names/citations so the new tests can build fixtures with those fields populated.
* kernel(overlay): ordered layer-merge algebra with diagnosticsLukasz Kasprzak2026-08-111-1/+100
| | | | | | | 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.
* test(layer): cover load's failure modes and same-date index orderingLukasz Kasprzak2026-08-111-1/+80
| | | | | | | | | | | | | load previously had zero coverage despite being the module's only fallible- I/O function and the one place the brief's literal code would have let an exception escape on this sexplib version (Failure instead of Parse_error). Add cases for a missing file, a malformed/unterminated sexp, an invalid slug embedded in an otherwise well-formed layer, and a success round-trip. Also add a same-date index case with three entries inserted out of slug order, pinning that the by-date index accumulates rather than overwrites and returns buckets in canonical order regardless of insertion order -- the previous two-entry fixture only ever touched bucket sizes 0 and 1.
* kernel(layer): sanctoral layer with canonical order and date indexLukasz Kasprzak2026-08-111-0/+45
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.