aboutsummaryrefslogtreecommitdiff
path: root/internal/calendar/calendar.go
Commit message (Collapse)AuthorAgeFilesLines
* perf(calendar): precompute the EF occupancy index alongside the transfer planperf/hoist-date-independent-workLukasz Kasprzak2026-08-241-41/+38
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The prior commit (ea6e598) memoised efTransferPlan but left the SAME occupiedByClass1/Or2 O(len(merged)) scan pattern in a second place: transferIfImpededEF's own fallback path, called from computeEF once per day for every class-1 candidate the (now cached) plan does not resolve -- confirmed by CPU profile, not assumed: computeEF.func1 (the old occupiedByRank closure) was ~62% of BenchmarkDays7EF's total time, almost all of it inside buildCelebration, called from BOTH efTransferPlan internally AND this second, uncached site. Before optimising, established precisely what "occupied" depends on, per the coordinator's warning that occupancy might genuinely mutate during planning (efTransferPlan's own `claimed` map suggested as much). It does not: occupiedByRank asks only "does some OTHER entry's ORIGINAL, untransferred date (buildCelebration + celebrationDate, which never considers a transfer) equal d" -- a pure function of (merged content, year, sel) alone, identical to what the transfer-plan cache already keys on. `claimed`, by contrast, genuinely mutates during one planning pass (it tracks which TARGET days a transfer walk has already assigned) and is NOT part of occupiedByRank's computation at all -- it remains computed fresh inside efTransferPlan every call, untouched by this change. These are independent, not the same thing wearing two names: efTransferPlan's own forward-walk loop already checks both, plus a third condition (the target day's own temporal class), as separate disjuncts. Given that, an occupancy INDEX -- not a second cache, and not a "we already know the answer" shortcut derived from plan's absence (which would have been correct for the transferIfImpededEF fallback's own control flow ONLY by coincidence: it ignores the unconditional All Souls Sunday-transfer special case that runs before the class-1 check on ANY rank, so a shortcut skipping straight past it would misfire the moment a user overlay retagged All Souls class-1, however unlikely on shipped data) -- is the safe fix: buildEFOccupancyIndex does the same merged-scan ONCE, into date -> []{slug, rank}, and .occupied does the exact O(1)-ish lookup + tiny-list filter occupiedByRank always computed, just precomputed. transferIfImpededEF's own signature, control flow and All Souls handling are completely unchanged; only what its two closure parameters read from changed. The index shares the transfer-plan cache's existing key (year, Selection, SHA-256 of merged) rather than adding a new one -- both are pure in exactly those three inputs, built in the same pass, so one key correctly covers both. Only the plan is copied per call (clonePlan); the index, which can hold one entry per merged slug (~330 on shipped data), is returned uncopied and documented immutable-after-construction -- safe under Go's concurrent-read guarantee since nothing anywhere writes to a returned index. New TestEFOccupancyIndexDetectsSameDateClass1Collision covers a path no existing test reached: two class-1 SANCTORAL entries sharing one ORIGINAL date (St Joseph/the Annunciation, covered by the prior commit's tests, collide via HOLY WEEK's temporal precedence on DIFFERENT dates, never with each other). A synthetic overlay (Compute's own public API, same style as the existing tests) puts two class-1 entries on the same otherwise-ordinary date: alone, either is simply observed; both together, RG 97/98 transfer both forward and the shared date reverts to its temporal office. Mutation-proved: dropping the index's exceptSlug self-exclusion (reverted after) made the test fail immediately -- every class-1 entry saw itself in the index and wrongly self-impeded, even the single-entry case. TestEFTransferPlanCacheConcurrentUse gained a third, disjoint key exercising this same collision path from goroutines alongside the two existing ones; `go test -race` on the whole package is clean. Benchmarked (interleaved before/after, same method throughout this branch): BenchmarkDays7EF drops from ~46-48ms (the prior commit's own plan-cache-only state) to ~15-18ms/op (allocs 208639 -> 98727, -53%; bytes 16.1MB -> 4.2MB, -74%) -- roughly a further 3x, ~4.6x cumulative against the original ~74ms. BenchmarkDaysWeek (OF, which never touches any of this) is unaffected: ~4.9-5.9ms/op both before and after, with byte-for-byte identical allocs/bytes in every run -- the ms-level wobble is machine noise, not a regression. A fresh CPU profile confirms the new remaining bottleneck precisely: writeSortedFields/hashMergedForPlan (the cache key's own SHA-256 of merged, ~330 entries) is now ~35% of total time, because it still runs on EVERY day (7x/week) to know whether a call is a cache hit, even though the work it gates now mostly isn't. Not fixed here: hoisting the key computation itself up to mobile.Days's batch level (mirroring the first commit on this branch, b6ee8f0) would need Compute's public signature to accept a precomputed key, a bigger surface change than this task's scope, reported rather than taken unilaterally. Output identity re-verified: the same 492-case sweep (both forms, both UI languages, all four corpora, the leap day/Triduum/Requiem/season- boundary dates, and the three Joseph/Annunciation years) is byte-identical (SHA-256-equal) before and after -- the same SHA-256 as the prior commit's own sweep, confirming zero output drift across the whole chain. go test ./... and make ci (both build tags, oracle/differential suite included) are green; go test -race on the whole internal/calendar package is clean.
* perf(calendar): memoise the EF transfer plan across a shared calendarLukasz Kasprzak2026-08-241-1/+8
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | computeEF calls efTransferPlan once per day, but the plan is pure in (year, the merged sanctoral content, Selection) and identical for every day sharing those three -- e.g. every date in one mobile.Days week. Rebuilding it per day was a real cost: efTransferPlan walks every merged entry looking for class-1 candidates, and for each one it considers, occupiedByClass1/Or2 (an occurrence check it also uses) walks merged AGAIN -- confirmed by CPU profile, not just by reading the code (github.com/lukaszkasprzak/lectio/internal/calendar.computeEF.func1, the occupiedByRank closure, at ~62% of BenchmarkDays7EF's total time before this fix, almost all of it inside buildCelebration). efTransferPlanCached (transfer_plan_cache.go) wraps efTransferPlan with a small, bounded, thread-safe LRU (container/list + sync.Mutex, capped at 64 entries -- gomobile may call in from multiple goroutines, and an unbounded map keyed by year would grow as a user scrolls through decades). The cache key is (year, Selection, a SHA-256 of merged's full content): merged is a map, so it cannot be a map key field itself, and Go's randomised map iteration order means two calls with identical content can visit it differently, so the hash sorts slugs and, within each entry, its Fields/Variant keys before hashing, and covers every field of every entry -- not just Rank/Date, the ones efTransferPlan's own read path happens to touch today, because which entries even qualify as class-1 is itself computed from that data, and occupiedByClass1/Or2 scan ALL of merged, not just the class-1 subset. Selection is included even though EF date resolution ignores it today (resolveDate never reads its sel parameter) -- keying on it costs nothing (four small strings) and protects a future change from silently poisoning a cache that never accounted for it. The returned map is always a fresh copy (clonePlan), never the cached instance, so sharing it across goroutines needs no further synchronisation. Verified the key is complete rather than trusted: with the content hash temporarily dropped from the key (mutation test, not committed), TestEFTransferPlanCacheInvalidatesOnOverlay failed immediately -- a plan warmed for the shipped 2008 calendar was wrongly served back for the same year with a user overlay applied (the Annunciation suppressed, which changes where the RG 96(a)/97/98 collision sends St Joseph: 31 March instead of 1 April, empirically confirmed against the pre-cache code before the test was written). TestEFTransferPlanCacheInvalidatesOnYear is a lighter companion covering the year field. TestEFTransferPlanCacheConcurrentUse hammers the cache from 12 goroutines across two different keys and reasserts correctness afterward; clean under `go test -race`. Benchmarked (interleaved before/after, same method as the readings.Prepare commit, to control for machine thermal drift): BenchmarkDays7EF drops from ~48-51ms to ~32-35ms/op (allocs 253810 -> 208639, -18%; bytes 28.1MB -> 16.1MB, -43%), roughly a third faster. BenchmarkDaysWeek (OF, which never calls efTransferPlan at all) is unaffected, ~3.5-4.1ms/op both before and after -- within noise, confirming this change is EF-only as intended. EF remains well outside OF's range (~32ms vs ~4ms), and a fresh CPU profile after this fix places the dominant remaining cost precisely: it is the SAME occupiedByClass1/Or2 pattern, but living OUTSIDE efTransferPlan -- transferIfImpededEF's own fallback path, called once per day for every class-1 candidate NOT already resolved by the (now cached) plan, i.e. the ordinarily-unimpeded ones (~15-20 of them), each triggering another O(len(merged)) scan. That call site was not part of what this task named, and memoising it is a materially different change (it is keyed per-candidate, not once per day), so it is reported here rather than folded into this commit. Output identity re-verified: a 492-case sweep of mobile.Day/mobile.Days (both forms, both UI languages, all four corpora, a leap day, the Sacred Triduum, a Requiem day, Christmas/Pentecost/Assumption/All Souls windows, and the three Joseph/Annunciation transfer-collision years this fix specifically touches -- 2008, 2035, 2046) produced byte-identical (SHA-256-equal) JSON before and after. go test ./... and make ci (both build tags, oracle/differential suite included) are green.
* fix(ef): two I-class feasts can no longer transfer onto the same dayLukasz Kasprzak2026-08-181-22/+119
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | RG 97/98 require a year's impeded I-class transfers to be resolved together. transferIfImpededEF resolved each one independently, so when an early Easter impedes both St Joseph (19 March) and the Annunciation (25 March), both walked to the Monday after Low Sunday -- and St Joseph, losing pickEF there, was observed on NO day of 2008, 2035 or 2046 at all. He appeared only as a commemoration in the Annunciation's tail. RG 96(a) settles it outright, and it is not a tie-break: the Annunciation transferred after Easter has a sedes propria -- "quando est transferendum post Pascha, transfertur, tamquam in sedem propriam, in feriam II post dominicam in albis". It claims the Monday by law; the rest queue in RG 98's order (table position, then the office impeded first) and walk around it. New efTransferPlan resolves the whole year's transfers as a set and hands each feast its target; transferIfImpededEF keeps the single-candidate walk and All Souls. The stale doc comment recording this as a known unfixable limitation is replaced -- its reasoning argued RG 97/98 tie-breaks, when RG 96(a) decides the only collision the universal calendar actually produces. 2008-04-01, 2035-04-03, 2046-04-03 ef-easter-2-tuesday -> St Joseph Matches colitur on all three. Mutation-tested: bypassing the plan reddens the new test on all three years. fix(ef): 13 January's Baptism is omitted when Holy Family falls on it The Holy Family Mass propers, verbatim in both photographic scans of the 1962 Missal: "Si festum S. Familiae occurrerit die 13 ianuarii, Missa dicitur de festo S. Familiae, sine commemoratione Baptismatis D.N.I.C., et sine commemoratione dominicae." Sine commemoratione: the Baptism is omitted outright, not merely outranked, so the candidate is dropped rather than left in Others. Structurally the same answer RG 91 entry 14 gives (Festa Domini II classis: primum mobilia, deinde fixa), but the propers' rubric is the direct authority for the omission. lectio kept the Baptism as the observed office on all seven such years in 2005-2050 -- 2008, 2013, 2019, 2030, 2036, 2041, 2047 -- and never observed the Holy Family Mass on 13 January at all. It already had the Holy Family Mass on every other Sunday after Epiphany, so this was the one date it could not reach. 13 rows of the 16801-day differential fixture change; colitur agrees with all of them.
* calendar: record the RG 95 chained-transfer gap in the sourceLukasz Kasprzak2026-08-121-0/+25
| | | | | | | | | | | | | | | | | | | | | | | | | | | | transferIfImpededEF resolves one candidate's own transfer walk in isolation and has no way to notice a SECOND, separately-transferred I-class candidate landing on the same destination day -- St Joseph (19 March) and the Annunciation (25 March) can both walk to the Monday after Low Sunday in the same year (2008, 2035, 2046, e.g. 2035-04-02: annunciation-of-the-blessed-virgin-mary +joseph-spouse- of-the-bl-virgin-mary). RG 95 grants the right of translation "solummodo festis I classis" to I-class feasts only, so both candidates genuinely have it, and the collision they land in together is an ordinary RG 97/98 occurrence question this function does not resolve: pickEF's plain alphabetical slug tie-break settles it instead of re-walking the loser. RG 98 itself supplies the determinism rule this collision needs and does not have: "in paritate autem Officium prius impeditum praecedit" -- at equal table position, the office impeded FIRST takes precedence, which is chronological (Joseph, impeded on the 19th, before the Annunciation's own walk begins on the 25th) and may favour Joseph over the current alphabetical fallback. No functional change -- this collision is left unfixed, defensible against scope (it needs resolving occurrence between two ALREADY- TRANSFERRED candidates, not a single one, a bigger shape than this function currently has). Leaving it unrecorded was not defensible: nothing in internal/calendar/ named RG 95, 97, 98, or chained transfers anywhere before this comment.
* calendar, precedence_ef: two more RG 91 gaps exposed by defect 4Lukasz Kasprzak2026-08-121-3/+26
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Found during the mandated 2005-2050 before/after verification (not one of the seven, but caused by defect 4's own fix, so fixed in the same pass rather than left as a self-introduced regression). Neither is a hypothetical: both are live in the fixed calendar and both were individually confirmed via cmd/lectio-ef-dump before being written up as tests. 1. RG 91 entry 4 (Immaculate Conception, Assumption BVM) sits ABOVE entry 6 (Sundays of Advent/Lent/Passiontide) -- unlike an ORDINARY I-class feast (entry 11, e.g. St Joseph), the Immaculate Conception (8 December) is not impeded by an Advent Sunday at all. Before defect 4, Advent Sundays were wrongly II class, so this was accidentally right (I beats II outright, no tie reached); once Sundays became I class the tie-break mattered for the first time, and precedenceEF had no branch for it -- the Sunday wrongly won. 2. RG 91 entry 5 (Vigil & Octave day of the Nativity) sits above entry 6 the same way, and its own gap was worse than a wrong winner: the Vigil of Christmas (24 December) falling on Advent IV, once defect 4 made that a genuine I-class tie, sent the Vigil into transferIfImpededEF's forward walk -- which has no way to re-place a transfer crossing the Dec 31/Jan 1 boundary (celebrationDate re-resolves a fixed date using the YEAR OF THE DAY BEING QUERIED, so a walk landing in the following January can never match the query that produced it). The Vigil did not move to the wrong day; it vanished for the whole year, for every year 24 December is a Sunday (2006, 2017, 2023, 2028, 2034, 2045). Both fixed the same way as the existing II-class-feast-of-the-Lord bonus in precedenceEF (a one-line precedence adjustment keyed on slug), generalised into a single beatsClass1Sunday helper covering both RG 91 entries. A separate, unrelated bug surfaced by the SAME verification pass and fixed alongside it: transferIfImpededEF's destination check reused precedenceEF's tie-break BAND to decide "is this day I or II class", but band encodes a different question (which of two EQUAL-class candidates wins a tie) -- an ordinary, non-Sunday II-class temporal candidate YIELDS under defect 2's own fix (band 5), even though it is genuinely II class. A day within the Octave of the Nativity (26-31 Dec, RG 67) is exactly such a day, so band<=3 alone let a transfer wrongly land inside it (a real reproduction: "vigil-of-christmas" would have landed on 29 December 2006 instead of vanishing outright, caught while tracing the entry-5 bug above). Replaced with a direct class test (isHighClass), which is both correct and simpler -- it no longer needs the Sunday flag at all for this particular check. Witnesses (precedence_ef_repro_test.go): TestImmaculateConceptionBeatsAdventSunday, TestVigilOfChristmasSurvivesAdventSunday. Fail before this commit with: 2013-12-08 observed = "ef-advent-sunday-2" want immaculate-conception-of-the-blessed-virgin-mary (RG 91 entry 4 beats entry 6) 2006-12-24 observed = "ef-advent-sunday-4" want vigil-of-christmas (RG 91 entry 5 beats entry 6; must not vanish) (full set: 2013/2019/2024 for the first, 2006/2017/2023/2028 for the second)
* calendar: RG 96 transfers must skip II-class days too (defect 1)Lukasz Kasprzak2026-08-121-12/+64
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | transferIfImpededEF's forward walk only skipped days whose temporal office was I class. RG 96 requires skipping to "the next following day that is not I or II class" -- II class also blocks. 2011: the Sacred Heart (Friday after the Corpus Christi octave) falls on 1 July and impedes the Precious Blood, also fixed on 1 July. The walk landed the Precious Blood on 2 July, displacing the Visitation (II class, fixed) outright, and never reached 3 July (an ordinary II-class Sunday) at all. Two changes were needed together, not one: 1. The walk's continuation threshold widens from "temporal band <= 2" to "<= 3", which is what actually captures every II-class temporal day (an ordinary Sunday, or a II-class named feast) as well as every I-class one -- band 3 is precedenceEF's own value for any non-Sunday-privileged II-class temporal candidate, unchanged by this commit. 2. A FIXED sanctoral II-class feast (the Visitation) has no temporal band at all -- the walk needs to also check whether some OTHER fixed celebration, class 1 or 2, already resolves onto the candidate day. computeEF now builds this check once (occupiedByRank, parameterised by which ranks count) and passes it in. Widening (1) alone is wrong on its own: a class-1 feast is NEVER actually impeded by a mere II-class day (I class always outranks II class outright, no tie exists) -- naively applying the wide "<=3" threshold to decide whether the ORIGINAL date is impeded, not just where to land afterwards, wrongly bumped unimpeded feasts landing on an ordinary Sunday (caught while testing this: All Saints, 1 Nov 2026, a Sunday that year, was wrongly pushed to 3 Nov). transferIfImpededEF now uses two different thresholds for two different questions -- class 1 only to decide IF a candidate is impeded at all, class 1 OR 2 to decide where an already-impeded one may land -- see its own doc comment for the full reasoning. Witness (precedence_ef_repro_test.go): TestTransferSkipsBothIAndIIClass. Fails before this commit with: 2011-07-02 observed = "precious-blood-of-our-lord-jesus-christ" want visitation-of-the-blessed-virgin-mary (RG 96: the Precious Blood must skip past it, not displace it) 2011-07-04 observed = "ef-time-after-pentecost-3-monday" want precious-blood-of-our-lord-jesus-christ (RG 96: first day that is neither I nor II class)
* fix(EF): fill reading gaps (major feasts, feria-repeat-through-feast, sundays)Lukasz Kasprzak2026-07-281-0/+13
| | | | | | | | | | | | | | | | EF readings had ~1.8% of days with no readings (major feasts falling back to the wrong Sunday, feria-repeats broken when the preceding Sunday was a feast). Now 0 gaps over 9496 days (2025-2050); reading correctness spot-checks match missalemeum except the resumed-Epiphany-Sunday edge (next commit). - calendar.TemporalSlug: the green-season weekday-repeats-the-Sunday rule uses the Sunday's TEMPORAL slug, so a Monday still finds its Sunday Mass when that Sunday was displaced by a feast (e.g. the Purification on Feb 2). - Add the missing EF temporal Masses (from missalemeum): the Nativity Day Mass (Heb 1:1-12 / John 1:1-14), Epiphany (Isa 60:1-6 / Matt 2:1-12), the Octave Day of Christmas (Titus 2:11-15 / Luke 2:21), the 6th Sunday after Epiphany (1 Thess 1:2-10 / Matt 13:31-35), and the last Sunday after Pentecost (Col 1:9-14 / Matt 24:15-35).
* fix(calendar): OF precedence/transfer + readings resolution; EF Pentecost octaveLukasz Kasprzak2026-07-281-0/+19
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | Fixes found by validating the offline engine per-day vs the LiturgicalCalendar API (OF) and missalemeum (EF) across 2005-2050. OF calendar is now 0 real errors in the forward window (season/cycle already perfect). OF precedence/transfer (calendar): - Ash Wednesday and Holy Week Mon-Wed are band-2 privileged, so a coinciding feast (Chair of St Peter on Ash Wednesday) is suppressed, not observed. - Solemnities transfer out of Holy Week / the Easter octave: the Annunciation defers to the Monday after the 2nd Sunday of Easter; St Joseph is anticipated to the Saturday before Palm Sunday; the Nativity of St John the Baptist moves to Jun 23 when a Lord's solemnity (Sacred Heart / Corpus Christi) falls Jun 24. - Within a precedence band, a solemnity of the Lord/BVM outranks a saint's (dignity tiebreak) rather than losing an alphabetical slug tie. - Perpetua & Felicity corrected optional -> obligatory memorial. OF readings resolution (bible.OFRef + lectionary data): - Verse-accurate Hebrew->Vulgate psalm mapping incl. the split psalms (9/10, 114/115, 116, 147); "+" verse joins and abbreviated ranges ("127-28") normalized; single-chapter books (2/3 John, Jude) get chapter 1. - Cleaned harvest artifacts from of-lectionary.ini (descriptive-suffix gospels, "or Year A" alternates, "*"/"[Vulg.]"/bracket markers, slash abbreviations); Joel/Malachi/Zechariah/Esther book-versification citations fixed to Vulgate. - Split a merged Ps 15:10/11 row in vul.tsv. Result: 0 unresolvable OF readings over 2557 rendered days (cycles A/B/C, varied Easters). EF: the Octave of Pentecost (Whit Monday-Saturday) is red, not white.
* feat(of): proper readings for the movable Marian memorialsLukasz Kasprzak2026-07-281-3/+9
| | | | | | | | Mary, Mother of the Church and the Immaculate Heart of Mary are computed in the engine (no fixed-date INI entry), so they fell back to the ferial. Attach their proper Masses inline: Mary MotC = Gen 3:9-15,20 / Ps 87 / John 19:25-34; Immaculate Heart = Isa 61:9-11 / 1 Sam 2 canticle / Luke 2:41-51. Verified they render when observed (Immaculate Heart 2029-06-09). Extends TestOFMovableMemorials.
* feat: complete OF+EF calendar coverage (movable feasts, vigils, ember days, ↵Lukasz Kasprzak2026-07-281-1/+61
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | transfers) Ordinary Form: - Add the movable Marian memorials that have no fixed date: Mary, Mother of the Church (Monday after Pentecost) and the Immaculate Heart (Saturday after the Sacred Heart), computed in computeOF. - General Norms 60: two coinciding obligatory memorials both become optional and the ferial is observed -- except Mary, Mother of the Church, which keeps precedence per its 2018 decree. Extraordinary Form: - Capture fixed-date vigils in the sanctoral harvest (Christmas, the Nativity of St John the Baptist, Sts Peter & Paul, St Lawrence, the Assumption, All Saints, the Immaculate Conception) -- previously filtered out. - temporalEF: the movable vigils of the Ascension (I class eve) and Pentecost (Whitsun Eve), and the September/Advent Ember days (II class violet ferias). - transferIfImpededEF: the Annunciation transfers past Holy Week/the Easter octave to the Monday after Low Sunday; All Souls moves to Nov 3 when Nov 2 is a Sunday. - genlect harvests proper Masses for the new Ember/vigil days. Adds TestEFCoverage + TestOFMovableMemorials. Bumps to 0.39.0. Verified per-day: OF 0 real errors vs calapi (2026-2028, 100% correct-observed); EF 0 real errors vs missalemeum (2025-2027), residual = deep-tail vigil-occurrence and St Joseph Passiontide-transfer edges (~1-2 days/yr, documented).
* feat(calendar): Compute branches on Form (computeEF via temporalEF/precedenceEF)Lukasz Kasprzak2026-07-271-3/+41
|
* refactor(calendar): Rank int enum -> unified string token (OF unchanged, ↵Lukasz Kasprzak2026-07-271-2/+5
| | | | | | | oracle 0 mismatches) Adds EF rank constants (class-1..4, commemoration); ofRankOrder() for the two ordered comparisons; buildCelebration defaults missing rank to ferial.
* fix(ini,calendar): full-line-only comments (preserve ';' in citations); drop ↵Lukasz Kasprzak2026-07-271-0/+3
| | | | superseded ferial from Others
* test(calendar): regression oracle vs calapi 2020-2040 (0 season mismatches)Lukasz Kasprzak2026-07-271-1/+1
| | | | | | Caught + fixed an Advent-start bug: anchor on Christmas Eve, not Dec 25, so years where Dec 25 is a Sunday don't lose the first week of Advent. Rank-class diffs (324) are informational — the initial sanctoral dataset is partial.
* feat(calendar): Compute — build celebrations, apply precedence + transfer, ↵Lukasz Kasprzak2026-07-271-0/+167
cycles Also: deterministic pick tiebreak by slug.