Skip to content

fix(calendar): eliminate monthly and leap-year recurrence drift in frontend views - #183

Merged
yetone merged 2 commits into
yetone:mainfrom
hasak21:fix/frontend-calendar-recurrence
Sep 5, 2026
Merged

fix(calendar): eliminate monthly and leap-year recurrence drift in frontend views#183
yetone merged 2 commits into
yetone:mainfrom
hasak21:fix/frontend-calendar-recurrence

Conversation

@hasak21

@hasak21 hasak21 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #182

Summary

In commit 21ea326 (#157), server-side recurrence calculation (server/src/recurrence.ts) was rewritten to calculate occurrences anchored on the seed with indexed arithmetic and clamping, preventing date drift on month-end, leap year, and bi-weekly weekday rules.

However, src/desktop/CalendarView.tsx and src/mobile/MobileCalendar.tsx still duplicated the old mutable stepRule logic (setMonth(getMonth() + interval)), causing client-side agenda views to diverge from the backend dispatcher:

  • Monthly on Jan 31: Client displayed 01-31, 03-03, 04-03... (February skipped, permanent drift to 3rd) while server fired 01-31, 02-28, 03-31....
  • Yearly on Feb 29: Client displayed 2028-02-29, 2029-03-01... (permanent drift to March 1) while server fired 2028-02-29, 2029-02-28... 2032-02-29.
  • Bi-weekly Mon/Wed: Client produced broken 9-day/12-day/9-day intervals instead of alternating bi-weekly pairs.

Changes

  • Created src/lib/recurrence.ts (nextOccurrenceOnOrAfter, occurrenceAt, addMonthsClamped, weeklyDays, addDays) mirroring the indexed date math from server/src/recurrence.ts.
  • Simplified nextOccurrence in both src/desktop/CalendarView.tsx and src/mobile/MobileCalendar.tsx to delegate to nextOccurrenceOnOrAfter.
  • Added unit tests in server/src/__tests__/frontend-calendar-recurrence.test.ts validating parity across all edge cases.

Verification

  • npm run lint: passed (checked 482 files, no issues)
  • npm run typecheck: passed
  • npm run server:typecheck: passed
  • npm run guard:big-brain && npm run guard:llm-tracked && npm run guard:engine-registry: passed
  • npm test: passed (1114 passing, 0 failing)
  • npm run build: passed (Vite production bundle built successfully)

…ontend views

Commit 21ea326 (yetone#157) fixed server-side recurrence calculation by anchoring
to the seed with indexed calculation (server/src/recurrence.ts) rather than
stepping from the previous occurrence.

However, CalendarView.tsx and MobileCalendar.tsx still used the old stepping
stepRule logic, causing client views to drift on month-ends (e.g. Jan 31 ->
March 3), leap years (Feb 29 -> March 1), and bi-weekly weekday rules.

This patch:
- Extracts pure recurrence math into src/lib/recurrence.ts.
- Unifies CalendarView.tsx and MobileCalendar.tsx to use nextOccurrenceOnOrAfter.
- Adds unit tests ensuring frontend recurrence matches backend dispatcher behavior.

Fixes yetone#182
@WhichPaths

Copy link
Copy Markdown
Collaborator

I wrote the server-side fix this mirrors (#157), so I checked the new copy the way I'd want mine checked: I ran all 17 cases from server/src/__tests__/calendar-recurrence.test.ts against src/lib/recurrence.ts and diffed the two implementations occurrence-by-occurrence.

Zero divergence. The 13 shapes your test file does not cover — monthly on the 30th, February clamping under interval: 2 across a leap year, mid-month monthly, weekly interval: 1 on Mon+Wed, the seed-week no-backfill rule, weekly with no byweekday, a duplicated/unsorted byweekday, an empty byweekday, daily with an interval, count including the seed, until inclusive, until terminating a clamped monthly series, and the far-future skip-ahead — all agree with the server exactly. The port is faithful.

One structural note, offered as a follow-up rather than a change request.

This PR resolves the drift by adding a second implementation and a second hand-written expectation table. That is the same shape that produced #182: server/src/recurrence.ts was fixed in #157, and nothing told the frontend copy about it for a release. After this lands there are two implementations and two tables, and the next change to the server's math has the same silent path back — update calendar-recurrence.test.ts, leave frontend-calendar-recurrence.test.ts as it is, and the two diverge again with CI green.

The asymmetry is already visible: 17 cases on one side, 4 on the other, transcribed by hand.

What would actually pin it is a differential test — run both implementations over one shared case table and assert they produce identical series, instead of asserting each against its own copy of the answers:

for (const c of CASES) {
  assert.deepEqual(series(serverImpl, c), series(frontendImpl, c), c.name)
}

That is what I ran to write this comment, and it needs no expected-value table at all — divergence is the failure, whichever side moved. It also survives a future change to the math: update the server, and the frontend test goes red instead of going quietly stale.

Worth noting your test file already proves the mechanism works — importing ../../../src/lib/recurrence.js from server/src/__tests__/ is what lets npm test reach frontend code at all, since the renderer has no runner of its own. A parity test rides the same trick.

I'm happy to send that as a follow-up once this merges — it is additive and shouldn't hold up the fix, which is correct as it stands.

@hasak21

hasak21 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review and verification, @WhichPaths! Fully agree on the differential parity test idea — looking forward to the follow-up once this lands!

@yetone
yetone merged commit 27d6eb8 into yetone:main Sep 5, 2026
7 checks passed
@yetone yetone mentioned this pull request Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(calendar): recurrence drift on month-end and leap years in desktop/mobile calendar views

3 participants