Repository navigation
Outlook sync: a Graph series mirrors as a series (structured recurrence → RRULE, exceptions, occurrence skip) - #919
Closed
NewWorldVenture wants to merge 8 commits into
Conversation
msEventToRow wrote recurrence_rule: null for every event and MsEvent had no type, seriesMasterId or originalStart, so a recurring Outlook event mirrored, and was published to every subscribed phone, as the one occasion it started on; an occurrence reaching the pull would have been mirrored beside its master as a second copy. - lib/sync/providers/graph-recurrence.ts: graphRecurrenceToRrule writes Graph's patternedRecurrence (daily, weekly, absoluteMonthly, relativeMonthly, absoluteYearly, relativeYearly; noEnd, endDate, numbered) as an RRULE. UNTIL is the last second of the end date in the series' own zone; Graph names zones the Windows way, so a curated Windows-to-IANA table (CLDR windowsZones) resolves them, falling back to the event's start zone, then UTC. An all-day series ends on a DATE. An unknown pattern yields null rather than an invented rule. - microsoft.ts: MsEvent gains type, seriesMasterId, originalStart and a typed recurrence. A seriesMaster carries its RRULE; an exception names its master and the slot it left (recurring_event_id, original_starts_at), which lib/sync/engine/exceptions folds into the master; the delta pull skips occurrences (isSeriesOccurrence), and only there: calendarView still expands them for the onboarding window. Stacked on the Google mirrored-series branch for NormalizedEvent's exception fields and the generic engine's fold pass. tests/an-outlook-series-mirrors-as-a-series.test.ts (17): the RRULE table, UNTIL in New York / Tokyo / unknown zone with and without a hint, all-day DATE UNTIL, unknown pattern, zone table; master/exception/plain mapping; the delta pull leaves occurrences out; the real generic engine with the real Microsoft adapter over a faked Graph mirrors the series once with the slots its exceptions left and the moved occurrence as an override (imported 3, skipped 1), which also exercises generic.ts's exception wiring. Controls: no RRULE fails 2; occurrences kept fails 2; exceptions forget their master fails 2; UNTIL in UTC fails 4. tsc and eslint clean; 32 sync suites 565/565. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
This was referenced Oct 3, 2026
…e/outlook-series-mirror-20261003 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…e/outlook-series-mirror-20261003 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…e/outlook-series-mirror-20261003 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…e/outlook-series-mirror-20261003
…e/outlook-series-mirror-20261003 Carries #908's fenced claim (and current main) down the stack. No conflicts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
…e/outlook-series-mirror-20261003 Carries #908's fenced chunk write (0490) down the stack into this unit. No conflicts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
NewWorldVenture
marked this pull request as ready for review
October 4, 2026 14:59
…ok-series-mirror-20261003 Carries the parent's refreshed head: the feed sync binds every row to the feed's own family, a CANCEL without DTSTART removes the occurrence it names, and the ICS parser keeps the EXDATE helpers beside its new options. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #918 (→ #913 → #908) for
NormalizedEvent's exception fields and the generic engine's fold pass. No migration: it writes the column 0483 adds.What was wrong
msEventToRowinlib/sync/providers/microsoft.tswroterecurrence_rule: nullfor every event, with a comment calling RRULE mapping a follow-up, andMsEventhad notype,seriesMasterIdororiginalStart. So a recurring Outlook event mirrored intosync_calendar_events, and was published by the feed to every subscribed phone, as the one occasion it started on. Had anoccurrencereached the delta pull it would have been mirrored beside its master as a second copy.Change
lib/sync/providers/graph-recurrence.ts(new):graphRecurrenceToRrulewrites Graph'spatternedRecurrenceas an RRULE: daily, weekly (BYDAY,WKST), absoluteMonthly (BYMONTHDAY), relativeMonthly (BYDAY+BYSETPOS,last→-1), absoluteYearly, relativeYearly;INTERVALwhen above one;COUNTfor a numbered range;UNTILfor an end date.UNTILon a timed series is the last second of the end date in the series' own zone. Graph names zones the Windows way ("Eastern Standard Time"), so a curated Windows→IANA table (CLDR windowsZones, the names Graph emits) resolves them, falling back to the event's own start zone, then UTC. An all-day series ends on aDATE, as RFC 5545 asks whenDTSTARTis a date. An unknown pattern yieldsnullrather than an invented rule.microsoft.ts:MsEventgainstype,seriesMasterId,originalStartand a typedrecurrence. AseriesMastercarries its RRULE; anexceptionnames its master and the slot it left (recurring_event_id,original_starts_at), whichlib/sync/engine/exceptions.tsfolds into the master'sexception_dates; the delta pull skipsoccurrenceitems (isSeriesOccurrence), and only there:calendarViewstill expands them for the onboarding import window.Tests
tests/an-outlook-series-mirrors-as-a-series.test.ts(17):UNTILin New York (20261001T035959Zfor an end date of 30 September), Tokyo, an unknown zone with and without the event's own zone as a hint, an all-dayUNTIL=20260930, unknown pattern →null, the zone table;FREQ=WEEKLY;BYDAY=WE;WKST=SU;UNTIL=20261001T035959Zand the two slots its exceptions left, the moved occurrence is its own row withrecurrence_id, the occurrence and the never-mirrored cancelled instance have no rows, the dentist is untouched (imported 3, skipped 1). This is the first end-to-end exercise ofgeneric.ts's exception wiring from Google sync: a mirrored series remembers the occurrences its source gave up (migration 0483) #918.Mutation controls (each reverted): no RRULE → 2 fail; occurrences kept → 2 fail; exceptions forget their master → 2 fail;
UNTILin UTC → 4 fail.tsc --noEmitand ESLint clean. 32 sync-related suites 565/565.Not done
rowToMsEvent) still sends no recurrence; a series created in Bubaly reaches Outlook as a single event, as before.claude/calendar-recurrence-exceptions-20261003when Google sync: a mirrored series remembers the occurrences its source gave up (migration 0483) #918 merges, and onward with the stack.🤖 Generated with Claude Code
https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
Generated by Claude Code