Warn when the years outrun the recurrence lookup tables (#61) #113

Merged
claude-bot merged 2 commits from feat/recurrence-horizon-guard into main 2026-09-04 16:59:56 +00:00
Contributor

Addresses #61 — the guard, the naming and the docs. Not the table extension; see below for why that is deliberate.

The problem

Six recurrences are table lookups rather than computations, all covering 2020–2035:

Table Events
HINDU_LUNAR_TABLE Diwali
LUNAR_NEW_YEAR_TABLE Lunar New Year
ASTRONOMICAL_DATES spring equinox, summer solstice, fall equinox, winter solstice

Past the last tabulated year they raise, upsert_yaml_events catches, logs one line per event, and continues. From 2036 those six events would simply be absent from the calendar with nothing to explain why.

Three changes

table_horizon() / tables_missing_year(), derived from the table data rather than hardcoded. The horizon is the minimum across tables — one running out is enough to drop its events, so extending only some of them changes nothing.

One aggregated warning instead of per-event lines lost in startup noise:

Recurrence horizon reached: 2036 is past the lookup tables (covered through 2035).
These events will be MISSING from the 2036 calendar: diwali, lunar_new_year,
winter_solstice. Exhausted tables: astronomical:fall_equinox, ... 
See docs/troubleshooting.md 'Recurrence horizon'.

GET /api/v1/recurrence-horizon + a header banner, so the condition is visible without reading logs. Naming the events is the point — "Diwali is missing" is actionable, "some events failed to resolve" is not.

The endpoint sits at /recurrence-horizon, not under /events/, whose /{event_id} route would shadow a literal segment unless declared first — the trap #93 fixed for /events/resolved.

Why the tables aren't extended here

#61 also asks to extend them to 2045. I checked whether that could be done from what is installed, and it can't:

  • holidays — already a dependency, and it stops at 2035 itself: holidays.country_holidays("IN") warns "Requested Holidays are available only from 2001 to 2035". It also returns no Chinese New Year entries. So the obvious escape hatch has the same wall.
  • astral — already a dependency, computes sunrise/sunset, but has no solstice or equinox function.

Extending properly needs an ephemeris library (skyfield/ephem) for the solstices, a Chinese-calendar library for Lunar New Year, and a panchanga source for Diwali, which is tithi-based. I'm not hand-writing 60 astronomical dates — silently wrong dates are worse than absent ones, and would be far harder to notice.

Filed as its own issue. docs/troubleshooting.md records both findings plus the useful bit for whoever takes it: validate any computation against the existing sixteen known years first — reproducing 2020–2035 exactly is good evidence it can be trusted onward.

Verification

  • Backend: 299 passed (5 new), ruff, ruff format, mypy clean; all seven pre-commit hooks pass.
  • Tests cover the horizon arithmetic, the named events for 2036, that 2026/2027 are clear, the warning text, and the endpoint.
  • Frontend in a container: tsc -b, eslint, vitest (76), npm run build all clean.

🤖 Generated with Claude Code

Addresses #61 — the guard, the naming and the docs. **Not** the table extension; see below for why that is deliberate. ## The problem Six recurrences are table lookups rather than computations, all covering 2020–2035: | Table | Events | |---|---| | `HINDU_LUNAR_TABLE` | Diwali | | `LUNAR_NEW_YEAR_TABLE` | Lunar New Year | | `ASTRONOMICAL_DATES` | spring equinox, summer solstice, fall equinox, winter solstice | Past the last tabulated year they raise, `upsert_yaml_events` catches, logs one line per event, and continues. From 2036 those six events would simply be **absent from the calendar** with nothing to explain why. ## Three changes **`table_horizon()` / `tables_missing_year()`**, derived from the table data rather than hardcoded. The horizon is the **minimum** across tables — one running out is enough to drop its events, so extending only some of them changes nothing. **One aggregated warning** instead of per-event lines lost in startup noise: ``` Recurrence horizon reached: 2036 is past the lookup tables (covered through 2035). These events will be MISSING from the 2036 calendar: diwali, lunar_new_year, winter_solstice. Exhausted tables: astronomical:fall_equinox, ... See docs/troubleshooting.md 'Recurrence horizon'. ``` **`GET /api/v1/recurrence-horizon` + a header banner**, so the condition is visible without reading logs. Naming the events is the point — *"Diwali is missing"* is actionable, *"some events failed to resolve"* is not. The endpoint sits at `/recurrence-horizon`, **not** under `/events/`, whose `/{event_id}` route would shadow a literal segment unless declared first — the trap #93 fixed for `/events/resolved`. ## Why the tables aren't extended here #61 also asks to extend them to 2045. I checked whether that could be done from what is installed, and it can't: - **`holidays`** — already a dependency, and it stops at 2035 *itself*: `holidays.country_holidays("IN")` warns *"Requested Holidays are available only from 2001 to 2035"*. It also returns no Chinese New Year entries. So the obvious escape hatch has the same wall. - **`astral`** — already a dependency, computes sunrise/sunset, but has no solstice or equinox function. Extending properly needs an ephemeris library (skyfield/ephem) for the solstices, a Chinese-calendar library for Lunar New Year, and a panchanga source for Diwali, which is tithi-based. **I'm not hand-writing 60 astronomical dates** — silently wrong dates are worse than absent ones, and would be far harder to notice. Filed as its own issue. `docs/troubleshooting.md` records both findings plus the useful bit for whoever takes it: validate any computation against the existing sixteen known years first — reproducing 2020–2035 exactly is good evidence it can be trusted onward. ## Verification - Backend: **299 passed** (5 new), `ruff`, `ruff format`, `mypy` clean; all seven pre-commit hooks pass. - Tests cover the horizon arithmetic, the named events for 2036, that 2026/2027 are clear, the warning text, and the endpoint. - Frontend in a container: `tsc -b`, `eslint`, `vitest` (76), `npm run build` all clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Warn when the years outrun the recurrence lookup tables (#61)
Some checks failed
CI / Pre-commit hooks (pull_request) Successful in 54s
CI / Python lint & type-check (pull_request) Successful in 1m4s
CI / Alembic migration check (pull_request) Successful in 1m50s
CI / Python tests (pull_request) Successful in 2m10s
CI / Frontend lint, test & build (pull_request) Successful in 3m10s
CI / Docker build, health smoke & E2E (pull_request) Failing after 2m5s
5a595b77b8
Six recurrences are table lookups rather than computations -- Diwali, Lunar
New Year, and the four solstices/equinoxes -- covering 2020-2035. Past the
last tabulated year they do not raise anything a user sees: `upsert_yaml_events`
catches the error, logs at warning level per event, and continues. From 2036
those events would simply be absent from the calendar with nothing to explain
why.

Three changes:

- `table_horizon()` and `tables_missing_year()` in recurrence.py, derived from
  the table data. The horizon is the *minimum* across tables, since one
  running out is enough to drop its events.
- The loader collects failures and emits one warning naming every affected
  event and every exhausted table, instead of a per-event line lost among
  startup noise. The per-event detail drops to debug.
- `GET /api/v1/recurrence-horizon` plus a header banner in the app shell, so
  the condition is visible without reading logs. Named events matter here:
  "Diwali is missing" is actionable, "some events failed to resolve" is not.

The endpoint deliberately sits at /recurrence-horizon rather than under
/events/, whose /{event_id} route would shadow a literal segment unless
declared first -- the trap #93 fixed for /events/resolved.

Not extending the tables here, which the issue also asked for. The dates
cannot be derived from what is installed: `holidays` warns "available only
from 2001 to 2035" for India and has no Chinese New Year entries, and
`astral` computes sunrise/sunset but no solstices. Doing it properly needs an
ephemeris library and a panchanga source, or hand-curated data from an
authority -- filed separately rather than guessed at here. Both findings are
recorded in docs/troubleshooting.md along with the advice to validate any
computation against the existing sixteen known years first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fix a racy E2E locator surfaced by the horizon banner
All checks were successful
CI / Pre-commit hooks (pull_request) Successful in 50s
CI / Python lint & type-check (pull_request) Successful in 1m14s
CI / Python tests (pull_request) Successful in 2m8s
CI / Alembic migration check (pull_request) Successful in 1m37s
CI / Frontend lint, test & build (pull_request) Successful in 3m18s
CI / Docker build, health smoke & E2E (pull_request) Successful in 2m15s
928df19a8e
The "clicking a day opens the side panel" E2E asserted on
`getByText(/Side panel|No events|January/i)`. That regex also matches the
calendar's own month headings, so once both the calendar and the panel had
rendered it resolved to two elements and tripped Playwright's strict mode:

    strict mode violation: ... resolved to 2 elements:
      1) <h2>January</h2>
      2) <div>No events on this date.</div>

It only ever passed by racing -- asserting in the window before the second
match appeared. Adding the horizon query to the app shell shifted render
timing enough to lose that race, which is how a latent test bug became a red
run.

The assertion now targets the panel itself rather than text that happens to
be near it. SidePanel's root becomes `<aside aria-label="Day details">`, so
the test can use `getByRole("complementary", ...)` -- a stable target that
does not depend on whether the selected day has events, and a named landmark
for screen readers, which the bare div was not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-bot deleted branch feat/recurrence-horizon-guard 2026-09-04 16:59:56 +00:00
Sign in to join this conversation.
No description provided.