fix(calendar): eliminate monthly and leap-year recurrence drift in frontend views - #183
Conversation
…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
|
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 Zero divergence. The 13 shapes your test file does not cover — monthly on the 30th, February clamping under 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: 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 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. |
|
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! |
Fixes #182
Summary
In commit
21ea326(#157), server-side recurrence calculation (server/src/recurrence.ts) was rewritten to calculate occurrences anchored on theseedwith indexed arithmetic and clamping, preventing date drift on month-end, leap year, and bi-weekly weekday rules.However,
src/desktop/CalendarView.tsxandsrc/mobile/MobileCalendar.tsxstill duplicated the old mutablestepRulelogic (setMonth(getMonth() + interval)), causing client-side agenda views to diverge from the backend dispatcher:01-31, 03-03, 04-03...(February skipped, permanent drift to 3rd) while server fired01-31, 02-28, 03-31....2028-02-29, 2029-03-01...(permanent drift to March 1) while server fired2028-02-29, 2029-02-28... 2032-02-29.Changes
src/lib/recurrence.ts(nextOccurrenceOnOrAfter,occurrenceAt,addMonthsClamped,weeklyDays,addDays) mirroring the indexed date math fromserver/src/recurrence.ts.nextOccurrencein bothsrc/desktop/CalendarView.tsxandsrc/mobile/MobileCalendar.tsxto delegate tonextOccurrenceOnOrAfter.server/src/__tests__/frontend-calendar-recurrence.test.tsvalidating parity across all edge cases.Verification
npm run lint: passed (checked 482 files, no issues)npm run typecheck: passednpm run server:typecheck: passednpm run guard:big-brain && npm run guard:llm-tracked && npm run guard:engine-registry: passednpm test: passed (1114 passing, 0 failing)npm run build: passed (Vite production bundle built successfully)