feat: calendar-mode comparison across editions (#311 / #312, #313) #472

Merged
manlycucumber merged 1 commit from feature/311-calendar-comparison into develop 2026-07-10 21:43:50 +00:00
manlycucumber commented 2026-07-10 21:41:14 +00:00 (Migrated from github.com)

What & why

Epic #311 — calendar-mode comparison: the cheapest, earliest comparison mode. Resolve a date, range, or sequence under two or more editions (1962/1954/1955 today) and report where they diverge. It needs no text layer — only ≥2 calendar engines — and is the pattern-setter for the v2.0 rite/office DiffResult.

The taxonomy (src/Compare/)

  • ComparisonField — the five text-free dimensions two calendars can differ on: feast (principal id), rank, colour, commemorations, season.
  • EditionDayCell — one edition's view of a date, built from the published day contract (so it diffs exactly what a consumer sees, never an internal shape).
  • ComparedDayN-way symmetric per-day diff: a field is divergent iff the cells don't all share one value (no privileged base). Commemorations compare as an order-independent set.
  • CalendarComparison — a range result with the divergentDayCount roll-up an "only-changed" filter needs.

The comparators

  • CalendarComparator (#312) — compareDay / compareRange. Selectors are normalised to edition urns and de-duplicated (1962/1960/roman:rubricae-1960 collapse); ≥2 distinct required.
  • SequenceComparator (#313) — acrossYears (a fixed civil date scrubbed through the movable cycle, skipping dates a year doesn't have) and acrossEditions; each ComparedSequence point is tagged with the fields changed from the previous point — powers the Site year-slider.
  • EditionResolver (internal) memoises the resolved year per (edition, year).

Verification

  • lint · analyse · test 1027 tests (1 skipped = live-oracle sweep). Purely additive — 0 existing files changed, so goldens are byte-identical.
  • Tests pinned to real engine divergences (each edition is itself validated vs the Divinum Officium oracle): Vigil of the Assumption II vs IV class; Octave of All Saints kept in 1954 vs feria in 1962; 4 Feb pulled onto Sexagesima in 2024 — plus a synthetic unit test of the divergence logic in isolation.
  • Adversarial review: GO, 0 correctness defects. Three pre-freeze API-soundness flags resolved in this PR: clarified the "text-free" boundary re the feast display name + a rank-scale caveat (docs/design/calendar-comparison-model.md); added an ordered editions list to ComparedDay::toArray() (JSON object key order isn't guaranteed); dropped the unused Overlay\CalendarCatalog DI param so the frozen constructor is clean.

Closes #312, #313.

## What & why Epic **#311 — calendar-mode comparison**: the cheapest, earliest comparison mode. Resolve a date, range, or sequence under two or more editions (1962/1954/1955 today) and report where they diverge. It needs **no text layer** — only ≥2 calendar engines — and is the **pattern-setter** for the v2.0 rite/office `DiffResult`. ## The taxonomy (`src/Compare/`) - **`ComparisonField`** — the five text-free dimensions two calendars can differ on: `feast` (principal id), `rank`, `colour`, `commemorations`, `season`. - **`EditionDayCell`** — one edition's view of a date, built from the **published day contract** (so it diffs exactly what a consumer sees, never an internal shape). - **`ComparedDay`** — **N-way symmetric** per-day diff: a field is divergent iff the cells don't all share one value (no privileged base). Commemorations compare as an order-independent set. - **`CalendarComparison`** — a range result with the `divergentDayCount` roll-up an "only-changed" filter needs. ## The comparators - **`CalendarComparator`** (#312) — `compareDay` / `compareRange`. Selectors are normalised to edition urns and de-duplicated (`1962`/`1960`/`roman:rubricae-1960` collapse); ≥2 distinct required. - **`SequenceComparator`** (#313) — `acrossYears` (a fixed civil date scrubbed through the movable cycle, skipping dates a year doesn't have) and `acrossEditions`; each `ComparedSequence` point is tagged with the fields changed from the previous point — powers the Site year-slider. - **`EditionResolver`** (internal) memoises the resolved year per `(edition, year)`. ## Verification - `lint` ✅ · `analyse` ✅ · `test` ✅ **1027 tests** (1 skipped = live-oracle sweep). Purely additive — 0 existing files changed, so goldens are byte-identical. - Tests pinned to **real engine divergences** (each edition is itself validated vs the Divinum Officium oracle): Vigil of the Assumption II vs IV class; Octave of All Saints kept in 1954 vs feria in 1962; 4 Feb pulled onto Sexagesima in 2024 — plus a synthetic unit test of the divergence logic in isolation. - Adversarial review: **GO, 0 correctness defects**. Three pre-freeze API-soundness flags resolved in this PR: clarified the "text-free" boundary re the feast display name + a rank-scale caveat (`docs/design/calendar-comparison-model.md`); added an ordered `editions` list to `ComparedDay::toArray()` (JSON object key order isn't guaranteed); dropped the unused `Overlay\CalendarCatalog` DI param so the frozen constructor is clean. Closes #312, #313.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Directorium/Core!472
No description provided.