Skip to content

feat(scripts): repair legacy-migrated series with wrong-frame BYDAY - #2535

Merged
tyler-dane merged 2 commits into
mainfrom
claude/repair-legacy-series-weekday
Aug 1, 2026
Merged

feat(scripts): repair legacy-migrated series with wrong-frame BYDAY#2535
tyler-dane merged 2 commits into
mainfrom
claude/repair-legacy-series-weekday

Conversation

@tyler-dane

Copy link
Copy Markdown
Contributor

Summary

Some recurring weekly series migrated into compass_sync from the legacy DB have their RRULE BYDAY captured in the wrong timezone frame relative to the event's own schedule.timeZone. Compass expands BYDAY in schedule.timeZone (occurrence-projection.ts floatingAnchor/floatingRules), so an affected series renders a phantom, wrong-weekday duplicate occurrence every week, alongside the correct one (which comes from a separately-imported exception record holding the real Google-sourced instant). Surfaced live via the founder's own "Review Week" series, reported after the #2525 crash fix shipped.

Adds a standalone, one-off CLI command — not an umzug migration, since packages/scripts/src/migrations/ only ever connects to prod_calendar, and compass_sync is a deliberately isolated database/credential. Mirrors the existing purge-corrupt-sync-events/purge-user pattern: connects via SyncMongoService, default dry-run, --apply to write.

For each affected weekly single-BYDAY series:

  1. Derives the true weekday from the strong-majority (>=80%) consensus of the series' own non-cancelled exceptions' real schedule.start — not a naive unanimous vote, which would wrongly skip a series with even one legitimate one-off reschedule buried in years of history (the founder's own series is 711 of 712 exceptions on Saturday, 1 on Sunday).
  2. Rewrites only the BYDAY= component of the rule — FREQ/INTERVAL/UNTIL/COUNT/WKST untouched.
  3. Shifts the series' own anchor date (schedule.start/end) only when it's on the wrong weekday AND no exception already covers that exact instant — otherwise that one historical date would keep generating its own phantom forever, since occurrence-projection.ts always materializes DTSTART regardless of BYDAY.
  4. Persists via EventRepository.replaceExisting (owner-scoped, schema-validated) and reprojects via the existing reprojectMaster/replaceForEvent machinery — the same transactional path every live user edit already goes through.

This is a local read-model correction only. Traced the full outbound path (command.routes.tsprovider-command.service.ts; the job dispatcher's commandApply/reconcile kinds have no producer in this build) — nothing watches a raw Mongo write and pushes it to Google.

Already run against production

Per the internal migration playbook's triage (idempotent per-doc repair, light ceremony — independent per-series writes, convergent on rerun, drives the already-transactional occurrence-replace path every live edit already races against):

  1. --dry-run against prod, cross-checked against an independent manual aggregation.
  2. Full report (all 2,016 candidate series, their outcome and confidence) shown to the founder for explicit go/no-go.
  3. --apply run: 37 series fixed across 9 tenants (all ≥93% exception consensus, 35 of 37 at 100%), 273 skipped as genuinely ambiguous or under-evidenced rather than guessed, 1,706 already correct.
  4. Verified: direct Mongo reads confirm the founder's two known-affected series now read BYDAY=SA and materialize exactly one occurrence per week; re-running in dry-run mode reports fixed=0 (full convergence, idempotent).

Cleanup plan

This is a one-off tool for a closed, already-migrated population (confirmed: the current write/import paths are frame-consistent, so this cannot recur for new data). Once this PR is confirmed merged and deployed, a follow-up PR deletes the command file, its subdirectory, and its cli.ts/cli.test.ts registration — mirroring how the 2026-07-29 Sync-cutover one-off tooling (migrate-provider-state.ts, preseed-sync.ts, etc.) was deleted the day after confirmed use, rather than accumulating as permanent dead code.

Test plan

  • bun test:scripts — 33 pass, including 8 new tests for the repair logic itself (BYDAY rewrite, anchor-date shift, the "already-excepted, no shift needed" case, strong-majority-with-outlier, ambiguous-history skip, no-exceptions skip, already-correct no-op, and idempotent rerun) and a new cli.test.ts case covering command dispatch.
  • bun run type-check — clean.
  • biome check — clean on all touched files.
  • Manual --dry-run/--apply/re---dry-run cycle against prod, described above.

🤖 Generated with Claude Code

tyler-dane and others added 2 commits August 1, 2026 12:08
Weekly recurring series migrated from the legacy DB during the 2026-07-29
Sync cutover had their RRULE BYDAY captured in the wrong timezone frame
relative to schedule.timeZone, causing Compass to materialize a phantom,
wrong-weekday occurrence every week alongside the correct one (imported
separately as an exception holding the real Google-sourced instant).

Adds a standalone CLI command (not an umzug migration - compass_sync is a
deliberately isolated database the migrate runner can't reach) that
detects affected weekly single-BYDAY series masters, derives the correct
weekday from the strong majority (>=80%) of the series' own exceptions
(tolerating rare legitimate one-off reschedules), rewrites BYDAY, shifts
the master's own anchor date only when no exception already covers it,
and reprojects via the existing reprojectMaster/replaceForEvent machinery.
Local read-model correction only - traced the full outbound path and
confirmed nothing pushes a raw Mongo write back to Google.

Run against prod: 37 series across 9 tenants fixed (all >=93% exception
consensus, most 100%), 273 skipped as genuinely ambiguous or
under-evidenced rather than guessed. Verified idempotent (rerun after
apply reports 0 further fixes) and spot-checked via direct read that
duplicate weekly occurrences are gone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cli.test.ts exercises each registered command's dispatch individually;
add the missing case for the new command alongside its siblings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tyler-dane
tyler-dane merged commit 2b3f4b8 into main Aug 1, 2026
20 checks passed
@tyler-dane
tyler-dane deleted the claude/repair-legacy-series-weekday branch August 1, 2026 18:14
tyler-dane added a commit that referenced this pull request Aug 1, 2026
The one-off repair (PR #2535) has run successfully against prod:
37 legacy-migrated weekly series fixed across 9 tenants (all >=93%
exception consensus, 35 of 37 at 100%), verified via direct DB reads
and an idempotent rerun reporting 0 further changes. The population it
targeted is closed - the current write/import paths are frame-consistent
and cannot reproduce it - so the command has nothing left to do and no
self-hosting relevance.

Mirrors how the 2026-07-29 Sync-cutover one-off tooling
(migrate-provider-state.ts, preseed-sync.ts, etc.) was deleted the day
after confirmed use (commit 07816a8), rather than accumulating as
permanent dead code. The full before/after data and methodology are
preserved in PR #2535's description.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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.

1 participant